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 function func from module Module 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:

  1. obtain sensitive data from the user (their credit card number)
  2. store the sensitive data in a wrapper that still allows us to know enough about the wrapped value
  3. 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