Quickstart¶
tl;dr¶
- Wrap any primitive value in
secret_type.secret
and use like you would have used the underlying value. - Your secret will now be protected: it can't be logged, and all operations on it are constant-time (to avoid timing attacks).
- 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!
- Secrets can be any primitive value
- Runtime exceptions prevent logging
- Operations derive new secrets
- Use
dangerous_apply
ordangerous_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. |
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)!
- We're using
Scrypt
as an example, butsecret
is not specific to thecryptography
package. - A random salt that does not need to be secret.
- We first use
cast
to convert thesecret
to abytes
object, then usedangerous_map
to run the key derivation function, wrapping the result in a newsecret
. - We must use
dangerous_reveal
to expose the actual key, which we persist to a database. - We retreive the plain-text key from the database, and immediately call
secret
on it to resume our protections. - Here we use
dangerous_apply
to run the key verification function, which raises if it fails. We use this instead ofdangerous_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 |
|
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.