View Source SensitiveData.Wrapper behaviour (Sensitive Data v0.1.0)

Defines a wrapper for sensitive data.

Using

When used, this module will implement the callbacks from this SensitiveData.Wrapper module, making the module where the use call is made into a sensitive data wrapper.

defmodule MySensitiveData do
  use SensitiveData.Wrapper
end

The options when using are:

Redacting and Labeling

It can be helpful to have some contextual information about the sensitive data contained within a wrapper. Aside from guards, you may wish to make use of:

  • redaction at the module level (i.e., single shared redaction logic for all terms wrapped by the same module)
  • labels at the instance level (i.e., each wrapper instance can have its own different label)

Beware

Redacting and labeling should be used with utmost care to ensure they won't leak any sensitive data under any circumstances.

If you use a custom redaction strategy, you must ensure it won't leak sensitive information for any possible sensitive term wrapped by the module.

If you allow labeling, you must ensure that any call site setting a label is doing so without leaking sensitive data.

Examples

defmodule CreditCard do
  use SensitiveData.Wrapper, allow_label: true, 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

# in IEx:
CreditCard.from(fn -> "5105105105105100" end, label: {:type, :debit})
#CreditCard<redacted: "5***********5100", label: {:type, :debit}, ...>

Both the redacted value and the label will be maintained as fields within the wrapper and can be used to assist in determining what the wrapped sensitive value was then the wrapper is inspected (manually when debugging, via Observer, dumped in crashes, and so on). Both values can be used in pattern matches.

For both redacting and labeling, nil values will not be displayed when inspecting.

Custom Failure Redaction

If an exception is raised within a sensitive context (such within exec/3, from/2, or map/3), both the exception and stack trace will be redacted. By default:

However, failure redaction can be customized via the :exception_redactor and :stacktrace_redactor options given to the use call.

Beware

Custom failure redaction should be used with utmost care to ensure it won't leak any sensitive data under any circumstances.

For exception redaction, you must ensure it won't leak sensitive information for any possible exception: standard Elixir ones, the ones in your code base, but also any exception that may be raised from a dependency.

For stack trace redaction, it must handle all possible stack traces.

If a custom redactor function fails, redaction will fall back to the corresponding default redactor listed above.

Summary

Types

Execution options.

The location of a function.

A wrapper target.

t()

A wrapper containing sensitive data.

Wrapping options.

A module implementing the SensitiveData.Wrapper behaviour.

Callbacks

Returns the result of the callback invoked with the sensitive term.

Wraps the sensitive term returned by the callback to prevent unwanted data leaks.

Invokes the callback on the wrapped sensitive term and returns the wrapped result.

Returns the sensitive term within wrapper.

Wraps the sensitive term to prevent unwanted data leaks.

Functions

Returns the length of the list wrapped within term.

Returns the size of the map wrapped within term.

Returns the size of a tuple wrapped within term.

Types

@type exec_opts() :: [{:into, spec()}]

Execution options.

Options

  • :into - a spec/0 value defining how the execution result should be wrapped.
@type function_handle() ::
  local_function :: atom() | {module(), remote_function :: atom()}

The location of a function.

If provided as an atom, the function is that with the same name and located in the current module.

If provided as a {module, atom} tuple, the function is that with the same name as atom and located in the module module.

@type spec() :: wrapper_module() | {wrapper_module(), wrap_opts()}

A wrapper target.

If provided as a wrapper_module module name, the from/2 callback in the corresponding wrapper module will be called with default options.

If provided as a {wrapper_module, wrap_opts} tuple, the from/2 callback in the corresponding wrapper module will be called with the provided wrap_opts options.

@type t() :: %{
  :__struct__ => atom(),
  :label => term(),
  :redacted => term(),
  optional(atom()) => term()
}

A wrapper containing sensitive data.

The wrapper structure should be considered opaque, aside from the label and redacted fields (see redacting and labeling section). You may read and match on those fields, but accessing any other fields or directly modifying any field is not advised.

Limited information regarding the contained sensitive data can be obtained via the guards in SensitiveData.Guards and the functions in SensitiveData.Wrapper.Util.

@type wrap_opts() :: [{:label, label :: term()}]

Wrapping options.

Allowable options are configured during use invocation, see Using section.

Invalid or unsupported values will be ignored and logged.

Options

  • :label - a label displayed when the wrapper is inspected. This option is only available if the :allow_label option was set to true when using SensitiveData.Wrapper.
@type wrapper_module() :: atom()

A module implementing the SensitiveData.Wrapper behaviour.

Callbacks

Link to this callback

exec(wrapper, function, exec_opts)

View Source
@callback exec(wrapper :: t(), (sensitive_data -> result), exec_opts()) :: result
when sensitive_data: term(), result: term()

Returns the result of the callback invoked with the sensitive term.

Executes the provided function with the sensitive term provided as the function argument, ensuring no data leaks in case of error.

The unwrapped result of the callback execution is then either returned as is, or wrapped according to provided options.

Options

See exec_opts/0.

Examples

# CreditCard implements the SensitiveData.Wrapper behaviour
credit_card = CreditCard.from(fn -> "5105105105105100" end)

# We can call a function that expect a credit card number (in string
# format), and will return `%{result: :ok}` upon successful payment:
%{result: :ok} = CreditCard.exec(credit_card, &pay_with_credit_card/1)

# We can also alter the wrapped data without ever exposing it outside
# of a sensitive context protecting the data from leaks:
# PaymentToken implements the SensitiveData.Wrapper behaviour
CreditCard.exec(credit_card, fn card_number -> tokenize(card_number) end, into: PaymentToken)
# #PaymentToken<...>
Link to this callback

from(function, wrap_opts)

View Source
@callback from(function(), wrap_opts()) :: t()

Wraps the sensitive term returned by the callback to prevent unwanted data leaks.

The callback is executed only once: during wrapper instanciation.

Options

See wrap_opts/0.

Examples

MySensitiveData.from(fn -> "foo" end)
# #MySensitiveData<...>

MySensitiveData.from(fn -> "5105105105105100" end, label: :credit_card_user_bob)
# #MySensitiveData<label: :credit_card_user_bob, ...>
Link to this callback

map(wrapper, function, wrap_opts)

View Source
@callback map(
  wrapper :: t(),
  (sensitive_data_orig -> sensitive_data_transformed),
  wrap_opts()
) :: t()
when sensitive_data_orig: term(), sensitive_data_transformed: term()

Invokes the callback on the wrapped sensitive term and returns the wrapped result.

Options

See wrap_opts/0.

Examples

data = MySensitiveData.from(fn -> "foo" end)

MySensitiveData.map(data, fn orig -> orig <> "bar" end, label: :now_foobar)
# #MySensitiveData<label: :now_foobar, ...>
Link to this callback

unwrap(wrapper)

View Source (optional)
@callback unwrap(wrapper :: t()) :: term()

Returns the sensitive term within wrapper.

By default, this optional callback will not be generated when SensitiveData.Wrapper is used.

Avoid Unwrapping Sensitive Data

Calling this function should be discouraged: exec/3 should be used instead to interact with sensitive data.

You can always obtain the raw sensitive data via exec(wrapped_value, & &1) but should seriously reconsider if that's needed: usually a combination of map/2 and exec/2 should satisfy all your needs regarding sensitive data interaction, and sensitive data typically never needs to be extracted from wrappers.

Examples

data = MySensitiveData.from(fn -> "foo" end)

# Not recommended
MySensitiveData.unwrap(data)
# "foo"

# Do this instead if you absolutely must unwrap the value
MySensitiveData.exec(data, & &1)
# "foo"
Link to this callback

wrap(term, wrap_opts)

View Source (optional)
@callback wrap(term(), wrap_opts()) :: t()

Wraps the sensitive term to prevent unwanted data leaks.

Prefer Using From

Calling this function should be discouraged: from/2 should be used instead to wrap sensitive data, as in all cases where you would call wrap(my_value) you should instead call from(fn -> my_value end).

The reason for this preference for from/2 is that it is much harder to accidentally misuse. For example, wrap(get_credit_card_number()) and from(get_credit_card_number) look very similar, but if get_credit_card_number/0 raises and leaks sensitive data when it does, the call to wrap/1 will raise and expose sensitive information whereas the call to from/1 will not. This is because from/2 internally wraps callback execution within SensitiveData.exec/2.

Additionally, making use of wrap/2 means you have access to unwrapped sensitive data, which should be discouraged: regardless of whether this sensitive data was fetched, generated, or derived, this should be done via functions from this library. Put another way, obtaining sensitive data and only then stuffing it into a wrapper is an anti-pattern that shouldn't be encouraged, and wrap/2 facilitates it.

Options

See wrap_opts/0

Examples

# Not recommended
MySensitiveData.wrap("foo")
# #MySensitiveData<...>

# Do this instead
MySensitiveData.from(fn -> "foo" end)
# #MySensitiveData<...>

# Not recommended
MySensitiveData.wrap("5105105105105100", label: :credit_card_user_bob)
# #MySensitiveData<label: :credit_card_user_bob, ...>

# Do this instead
MySensitiveData.from(fn -> "5105105105105100" end, label: :credit_card_user_bob)
# #MySensitiveData<label: :credit_card_user_bob, ...>

Functions

@spec sensitive_length(t()) :: non_neg_integer()

Returns the length of the list wrapped within term.

Link to this function

sensitive_map_size(term)

View Source
@spec sensitive_map_size(t()) :: non_neg_integer()

Returns the size of the map wrapped within term.

The size of a map is the number of key-value pairs that the map contains.

This operation happens in constant time.

Link to this function

sensitive_tuple_size(term)

View Source
@spec sensitive_tuple_size(t()) :: non_neg_integer()

Returns the size of a tuple wrapped within term.

This operation happens in constant time.