Skip to content

Quickstart

tl;dr

  1. Wrap any primitive value in secret_type.secret and use like you would have used the underlying value.
  2. Your secret will now be protected: it can't be logged, and all operations on it are constant-time (to avoid timing attacks).
  3. See operations for unwrapping and using the value. Anything you do involving a secret will create a new secret.
>>> from secret_type import secret
>>> password = secret("a very secret value") # (1)!

>>> print(password) # (2)!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "secret_type/containers/secret.py", line 91, in __str__
  raise SecretException()
secret_type.exceptions.SecretException: Secrets cannot be examined

>>> better_password = password + "!" # (3)!
>>> >>> type(better_password)
<class 'secret_type.sequence.SecretStr'>

>>> better_password.dangerous_apply(print) # (4)!
a very secret value!
  1. Secrets can be any primitive value
  2. Runtime exceptions prevent logging
  3. Operations derive new secrets
  4. Use dangerous_apply or dangerous_map to access the underlying value ```

One Function to Rule Them All

secret_type.secret(o: T) -> Secret[T]

This single function provides a convenient to indicate that a value is considered sensitive.

Sensitive values can be any (subclass of a) non-container primitive. Simply wrap the value in a call to secret, and off you go! Any operations you do on that wrapped value will also return a secret.

ATTRIBUTE DESCRIPTION
o

The value to mark as secret.

TYPE: Union[str, bytes, int, float, bool]

Example:

Here is an example using secret to protect a key derived from a user password. We want to make sure that no one is able to accidentally log either of these values, that all comparison operations are constant-time, and that any derived values are also secret.

from secret_type import secret
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt # (1)!

salt = os.urandom(16) # (2)!

def derive_key(user_password: str):
  password = secret(user_password)
  kdf = Scrypt(salt=salt, length=32, n=2**14, r=8, p=1)

  key = password.cast(bytes).dangerous_map(kdf.derive) # (3)!

  with key.dangerous_reveal() as key: # (4)!
    persist_to_database(key)

# Some time later...

def check_password(entered_password: str):
  password = secret(entered_password)
  kdf = Scrypt(salt=salt, length=32, n=2**14, r=8, p=1)

  key = secret(get_key_from_database()) # (5)!
  password.cast(bytes).dangerous_apply(kdf.verify, key) # (6)!
  1. We're using Scrypt as an example, but secret is not specific to the cryptography package.
  2. A random salt that does not need to be secret.
  3. We first use cast to convert the secret to a bytes object, then use dangerous_map to run the key derivation function, wrapping the result in a new secret.
  4. We must use dangerous_reveal to expose the actual key, which we persist to a database.
  5. We retreive the plain-text key from the database, and immediately call secret on it to resume our protections.
  6. Here we use dangerous_apply to run the key verification function, which raises if it fails. We use this instead of dangerous_map as we don't care about the return value.
Source code in secret_type/__init__.py
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def secret(o: T) -> Secret[T]:
    """This single function provides a convenient to indicate that a value is considered sensitive.

    Sensitive values can be any (subclass of a) non-container primitive.
    Simply wrap the value in a call to `secret`, and off you go!
    Any operations you do on that wrapped value will also return a `secret`.

    Attributes:
      o (Union[str, bytes, int, float, bool]): The value to mark as secret.

    Examples: Example:
      Here is an example using `secret` to protect a key derived from a user password.
      We want to make sure that no one is able to accidentally log either of these values,
      that all comparison operations are constant-time, and that any derived values are also secret.

      ```python
      from secret_type import secret
      from cryptography.hazmat.primitives.kdf.scrypt import Scrypt # (1)!

      salt = os.urandom(16) # (2)!

      def derive_key(user_password: str):
        password = secret(user_password)
        kdf = Scrypt(salt=salt, length=32, n=2**14, r=8, p=1)

        key = password.cast(bytes).dangerous_map(kdf.derive) # (3)!

        with key.dangerous_reveal() as key: # (4)!
          persist_to_database(key)

      # Some time later...

      def check_password(entered_password: str):
        password = secret(entered_password)
        kdf = Scrypt(salt=salt, length=32, n=2**14, r=8, p=1)

        key = secret(get_key_from_database()) # (5)!
        password.cast(bytes).dangerous_apply(kdf.verify, key) # (6)!
      ```

      1. We're using `Scrypt` as an example, but `secret` is not specific to the `cryptography` package.
      2. A random salt that does not need to be secret.
      3. We first use [`cast`][secret_type.Secret.cast] to convert the `secret` to a `bytes` object,
        then use [`dangerous_map`][secret_type.Secret.dangerous_map] to run the key derivation function,
        wrapping the result in a new `secret`.
      4. We must use [`dangerous_reveal`][secret_type.Secret.dangerous_reveal] to expose the actual key,
        which we persist to a database.
      5. We retreive the plain-text key from the database, and immediately call `secret` on it to resume
        our protections.
      6. Here we use [`dangerous_apply`][secret_type.Secret.dangerous_apply] to run the key verification function,
        which raises if it fails. We use this instead of [`dangerous_map`][secret_type.Secret.dangerous_map] as we don't care about the return value.
    """
    return Secret.wrap(o)

Operations

Of course, to actually be useful, secrets must at some point be used, which necessitates them being in plain-text. There are three ways to use the underlying value of a secret.

1. Derive a new secret

dangerous_map takes a function that operates on the underlying value, and returns a new value to protect.

    new_secret = secret("foo").dangerous_map(lambda x: x + "bar")

2. Apply a side-effect

dangerous_apply takes a function that operates on the underlying value, and discards the return value.

    secret("foo").dangerous_apply(logger.info)

3. Access the raw value

dangerous_reveal is a context manager that yields the underlying value.

    with secret("foo").dangerous_reveal() as value:
    assert value == "foo"

More Docs

For more details, see the Secret class.