View Source Cheatsheet
Creating a Wrapper
defmodule MySecretData do
use SensitiveData.Wrapper
end
Getting Data into a Wrapper
SensitiveData exec/2
SensitiveData.exec(
fn -> System.fetch_env!("DB_URI") end,
into: MySecretData)
Wrapper from/2
MySecretData.from(fn -> System.fetch_env!("DB_URI") end)
From :stdio
SensitiveData.gets_sensitive("Your password: ",
into: MySecretData)
or alternatively
MySecretData.from(fn ->
SensitiveData.gets_sensitive("Your password: ")
end)
Interacting with Wrapped Data
map/3
The sensitive data within a container can easily be modified using SensitiveData.Wrapper.map/3
:
# get authentication data in Basic Authentication format
basic_credentials = MySecretData.from(fn ->
System.fetch_env!("BASIC_AUTH") end)
# later, these credentials need to be converted
# to username & password
map_credentials =
MySecretData.map(basic_credentials, fn basic ->
[username, password] =
basic
|> Base.decode64!()
|> String.split(":")
end)
exec/3
Executing functions requiring sensitive data located in a wrapper can be
accomplished with SensitiveData.Wrapper.exec/3
:
# get authentication data
credentials = MySecretData.from(fn ->
System.fetch_env!("BASIC_AUTH") end)
# later, make an API request using these credentials
{:ok, data} =
MySecretData.exec(credentials, fn basic_auth ->
get_api_results("/some/endpoint", basic_auth)
end)
Identifying Wrapper Contents
It is often useful to be able to determine some information about the sensitive data contained within wrappers, both for programmatic and human debuggability reasons.
Guards and Utilities
Functions from the SensitiveData.Guards
module and utility functions from
the SensitiveData.Wrapper
module can help with branching logic depending
(somewhat) on the sensitive data within the wrappers:
require SensitiveData.Guards
alias SensitiveData.Guards
alias SensitiveData.Wrapper
data = MySecretData.from(fn ->
%{foo: :yes, bar: :no} end)
case data do
map when Guards.is_sensitive_map(map) ->
IO.puts("It's a map with #{Wrapper.sensitive_map_size(map)} items")
_ ->
IO.puts("It's not a map")
end
Redaction
It can be useful to have a redacted version of sensitive data, which can be defined at the module
level with the redactor
option given to use SensitiveData.Wrapper
, which can be given as:
- an atom - the name of the redactor function located in the same module
- a
{Module, func}
tuple - the redactor functionfunc
from moduleModule
will be used for redaction
In either case, the redaction function will be called with a single argument: the sensitive term.
⚠️️ BEWARE ⚠️ If you use a custom redaction strategy, you must ensure it won't leak any sensitive data under any circumstances.
defmodule CreditCard do
use SensitiveData.Wrapper, redactor: :redactor
def redactor(card_number) when is_binary(card_number) do
{<<first_number::binary-1, to_mask::binary>>, last_four} = String.split_at(card_number, -4)
IO.iodata_to_binary([first_number, List.duplicate("*", String.length(to_mask)), last_four])
end
end
Alternatively, the same result can be achieved with:
defmodule MyApp.Redaction do
def redact_credit_card(card_number) when is_binary(card_number) do
{<<first_number::binary-1, to_mask::binary>>, last_four} = String.split_at(card_number, -4)
IO.iodata_to_binary([first_number, List.duplicate("*", String.length(to_mask)), last_four])
end
end
defmodule CreditCard do
use SensitiveData.Wrapper, redactor: {MyApp.Redaction, :redact_credit_card}
end
In use (either implementation):
iex(1)> cc = CreditCard.from(fn -> "5105105105105100" end)
#CreditCard<redacted: "5***********5100", ...>
iex(2)> cc.redacted
"5***********5100"
Labeling
It can be useful to label sensitive data, for example to give scope to the given data. Labeling
must be explicitly enabled via the allow_label: true
option given to use SensitiveData.Wrapper
.
Once enabled at the module level, each instance of sensitive data can be labeled via the :label
option.
⚠️ BEWARE ⚠️ If you allow labels, you must ensure call sites aren't leaking sensitive data via label values.
defmodule DatabaseCredentials do
use SensitiveData.Wrapper, allow_label: true
end
In use:
iex(1)> db = DatabaseCredentials.from(fn -> System.fetch_env("PROD_DB_CREDS") end, label: :prod)
#DatabaseCredentials<label: :prod, ...>
iex(2)> db.label
:prod
iex(3)> DatabaseCredentials.from(fn -> System.fetch_env("PROD_CI_CREDS") end, label: :integration)
#DatabaseCredentials<label: :integration, ...>
If labels aren't allowed but one is passed as an option, the label will be ignored and a warning will be logged.
One-Off Executions
In certain cases, there's is only a transient need for sensitive data, such as when connecting to a system: once the connection is made, the credentials aren't needed anymore.
{:ok, pid} =
SensitiveData.exec(fn ->
"DATABASE_CONNECTION_URI"
|> System.fetch_env!()
|> parse_postgres_uri()
|> Postgrex.start_link()
end)
Multiple Wrapper Modules
It can be useful to use several module implementing the SensitiveData.Wrapper
behaviour, for example to benefit from different redaction capabilities.
Let's take as an example a credit card number which will be converted into a payment token which can then be used to charge the user. It would be useful to be able to "peek" at non-sensitive data (to display to the user in the UI, for debugging, and so on): this can be achieved via redaction. Naturally, as the credit card and associated token are different, they'll require different redaction implementations:
defmodule CreditCard do
use SensitiveData.Wrapper, redactor: :redactor
def redactor(card_number) when is_binary(card_number) do
{<<first_number::binary-1, to_mask::binary>>, last_four} = String.split_at(card_number, -4)
IO.iodata_to_binary([first_number, List.duplicate("*", String.length(to_mask)), last_four])
end
end
defmodule PaymentToken do
use SensitiveData.Wrapper, redactor: :redactor
def redactor(token) when is_binary(token) do
# the first 10 characters aren't considered sensitive
{not_sensitive, rest} = String.split_at(token, 10)
IO.iodata_to_binary([not_sensitive, List.duplicate("*", String.length(rest))])
end
end
For completeness, let's use the following for tokenization logic:
defmodule Tokenizer do
def tokenize(card_number) when is_binary(card_number) do
"TOK_" <> random_string(20)
end
defp random_string(length) do
:crypto.strong_rand_bytes(length)
|> Base.url_encode64
|> binary_part(0, length)
end
end
Note how the Tokenizer
module contains only "normal" code: it has no knowledge
of the existence of any wrapped values.
We can now easily do the following:
- obtain sensitive data from the user (their credit card number)
- store the sensitive data in a wrapper that still allows us to know enough about the wrapped value
- convert the sensitive data to a separate type (and different wrapper) while retaining the ability to inspect non-sensitive portions
"Please enter your credit card number: "
SensitiveData.gets_sensitive(into: CreditCard)
|> IO.inspect()
|> CreditCard.exec(&Tokenizer.tokenize/1, into: PaymentToken)
Which will output:
#CreditCard<redacted: "5***********5100", ...>
#PaymentToken<redacted: "TOK_QTzY5m**************", ...>
And of course we can use guards in our code if we want to ensure only wrapped data of the proper type is used:
import SensitiveData.Guards, only: [is_sensitive: 2]
def pay(token) when is_sensitive(token, PaymentToken) do
# make the payment
end
def pay(credit_card) when is_sensitive(credit_card, CreditCard) do
credit_card
|> CreditCard.exec(& &1, into: PaymentToken)
|> pay()
end