derivepassphrase.vault
¶
Python port of the vault(1) password generation scheme.
Vault
¶
Vault(
*,
phrase: Buffer | str = b"",
length: int = 20,
repeat: int = 0,
lower: int | None = None,
upper: int | None = None,
number: int | None = None,
space: int | None = None,
dash: int | None = None,
symbol: int | None = None
)
A work-alike of James Coglan’s vault.
Store settings for generating (actually: deriving) passphrases for named services, with various constraints, given only a master passphrase. Also, actually generate the passphrase. The derivation is deterministic and non-secret; only the master passphrase need be kept secret. The implementation is compatible with vault.
James Coglan explains the passphrase derivation algorithm in great detail in his blog post on said topic: A principally infinite bit stream is obtained by running a key-derivation function on the master passphrase and the service name, then this bit stream is fed into a sequin.Sequin to generate random numbers in the correct range, and finally these random numbers select passphrase characters until the desired length is reached.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
phrase
|
Buffer | str
|
The master passphrase from which to derive the service passphrases. If a string, then the UTF-8 encoding of the string is used. |
b''
|
length
|
int
|
Desired passphrase length. |
20
|
repeat
|
int
|
The maximum number of immediate character repetitions allowed in the passphrase. Disabled if set to 0. |
0
|
lower
|
int | None
|
Optional constraint on ASCII lowercase characters. If positive, include this many lowercase characters somewhere in the passphrase. If 0, avoid lowercase characters altogether. |
None
|
upper
|
int | None
|
Same as |
None
|
number
|
int | None
|
Same as |
None
|
space
|
int | None
|
Same as |
None
|
dash
|
int | None
|
Same as |
None
|
symbol
|
int | None
|
Same as |
None
|
Raises:
Type | Description |
---|---|
ValueError
|
Conflicting passphrase constraints. Permit more characters, or increase the desired passphrase length. |
Warning
Because of repetition constraints, it is not always possible to detect conflicting passphrase constraints at construction time.
UUID
class-attribute
instance-attribute
¶
UUID: Final = b'e87eb0f4-34cb-46b9-93ad-766c5ab063e7'
A tag used by vault in the bit stream generation.
CHARSETS
class-attribute
instance-attribute
¶
CHARSETS: Final = MappingProxyType(
OrderedDict(
[
("lower", b"abcdefghijklmnopqrstuvwxyz"),
("upper", b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
(
"alpha",
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
),
("number", b"0123456789"),
(
"alphanum",
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
),
("space", b" "),
("dash", b"-_"),
(
"symbol",
b"!\"#$%&'()*+,./:;<=>?@[\\]^{|}~-_",
),
(
"all",
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 !\"#$%&'()*+,./:;<=>?@[\\]^{|}~-_",
),
]
)
)
Known character sets from which to draw passphrase characters. Relies on a certain, fixed order for their definition and their contents.
create_hash
classmethod
¶
Create a pseudorandom byte stream from phrase and service.
Create a pseudorandom byte stream from phrase
and service
by
feeding them into the key-derivation function PBKDF2
(8 iterations, using SHA-1).
Parameters:
Name | Type | Description | Default |
---|---|---|---|
phrase
|
Buffer | str
|
A master passphrase, or sometimes an SSH signature. Used as the key for PBKDF2, the underlying cryptographic primitive. If a string, then the UTF-8 encoding of the string is used. |
required |
service
|
Buffer | str
|
A vault service name. Will be suffixed with the
|
required |
length
|
int
|
The length of the byte stream to generate. |
32
|
Returns:
Type | Description |
---|---|
bytes
|
A pseudorandom byte string of length |
Note
Shorter values returned from this method (with the same key and message) are prefixes of longer values returned from this method. (This property is inherited from the underlying PBKDF2 function.) It is thus safe (if slow) to call this method with the same input with ever-increasing target lengths.
Examples:
>>> # See also Vault.phrase_from_key examples.
>>> phrase = bytes.fromhex('''
... 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
... 00 00 00 40
... f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86
... 66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd
... 0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c
... 1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02
... ''')
>>> Vault.create_hash(phrase, 'some_service', length=4)
b'M\xb1<S'
>>> Vault.create_hash(phrase, b'some_service', length=16)
b'M\xb1<S\x827E\xd1M\xaf\xf8~\xc8n\x10\xcc'
>>> Vault.create_hash(phrase, b'NOSUCHSERVICE', length=16)
b'\x1c\xc3\x9c\xd9\xb6\x1a\x99CS\x07\xc41\xf4\x85#s'
generate
¶
Generate a service passphrase.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
service_name
|
Buffer | str
|
The service name. If a string, then the UTF-8 encoding of the string is used. |
required |
phrase
|
Buffer | str
|
If given, override the passphrase given during construction. If a string, then the UTF-8 encoding of the string is used. |
b''
|
Returns:
Type | Description |
---|---|
bytes
|
The service passphrase. |
Raises:
Type | Description |
---|---|
ValueError
|
Conflicting passphrase constraints. Permit more characters, or increase the desired passphrase length. |
Examples:
>>> phrase = b'She cells C shells bye the sea shoars'
>>> # Using default options in constructor.
>>> Vault(phrase=phrase).generate(b'google')
b': 4TVH#5:aZl8LueOT\\{'
>>> # Also possible:
>>> Vault().generate(b'google', phrase=phrase)
b': 4TVH#5:aZl8LueOT\\{'
Conflicting constraints are sometimes only found during generation.
>>> # Note: no error here...
>>> v = Vault(
... lower=0,
... upper=0,
... number=0,
... space=2,
... dash=0,
... symbol=1,
... repeat=2,
... length=3,
... )
>>> # ... but here.
>>> v.generate(
... '0', phrase=b'\x00'
... )
Traceback (most recent call last):
...
ValueError: no allowed characters left
is_suitable_ssh_key
staticmethod
¶
is_suitable_ssh_key(
key: Buffer, /, *, client: SSHAgentClient | None = None
) -> bool
Check whether the key is suitable for passphrase derivation.
Some key types are guaranteed to be deterministic. Other keys types are only deterministic if the SSH agent supports this feature.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
key
|
Buffer
|
SSH public key to check. |
required |
client
|
SSHAgentClient | None
|
An optional SSH agent client to check for additional deterministic key types. If not given, assume no such types. |
None
|
Returns:
Type | Description |
---|---|
bool
|
True if and only if the key is guaranteed suitable for use in deriving a passphrase deterministically (perhaps restricted to the indicated SSH agent). |
phrase_from_key
classmethod
¶
phrase_from_key(
key: Buffer,
/,
*,
conn: SSHAgentClient | socket | None = None,
) -> bytes
Obtain the master passphrase from a configured SSH key.
vault allows the usage of certain SSH keys to derive a master
passphrase, by signing the vault UUID
with the SSH key.
The key type must ensure that signatures are deterministic
(perhaps only in conjunction with the given SSH agent).
Parameters:
Name | Type | Description | Default |
---|---|---|---|
key
|
Buffer
|
The (public) SSH key to use for signing. |
required |
conn
|
SSHAgentClient | socket | None
|
An optional connection hint to the SSH agent. See
|
None
|
Returns:
Type | Description |
---|---|
bytes
|
The signature of the vault |
Raises:
Type | Description |
---|---|
KeyError
|
|
NotImplementedError
|
|
OSError
|
|
ValueError
|
The SSH key is principally unsuitable for this use case. Usually this means that the signature is not deterministic. |
Examples:
>>> import base64
>>> # Actual Ed25519 test public key.
>>> public_key = bytes.fromhex('''
... 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
... 00 00 00 20
... 81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1
... 30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76
... ''')
>>> expected_sig_raw = bytes.fromhex('''
... 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
... 00 00 00 40
... f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86
... 66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd
... 0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c
... 1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02
... ''')
>>> # Raw Ed25519 signatures are 64 bytes long.
>>> signature_blob = expected_sig_raw[-64:]
>>> phrase = base64.standard_b64encode(signature_blob)
>>> Vault.phrase_from_key(phrase) == expected
True
phrases_are_interchangable
classmethod
¶
Return true if the passphrases are interchangable to Vault.
Vault internally passes the passphrase as the key to HMAC-SHA1. HMAC requires keys to have a certain fixed length, and therefore transforms keys of other lengths suitably. Because of this, in general, there exist multiple passphrases that behave identically under Vault.
HMAC key transformation
Keys strictly larger than the SHA1 block size (64 bytes) are first hashed with SHA1, then the digest is used in place of the original key. Then, any keys/digests smaller than the block size are padded with NUL bytes on the right, up to the block size.
As a result, keys smaller than the block size are padded, keys larger than the block size are hashed and then padded, and keys exactly as large as the block size are used as-is.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
phrase1
|
Buffer
|
A passphrase to compare. Must be a binary string to mitigate timing attacks. |
required |
phrase2
|
Buffer
|
A passphrase to compare. Must be a binary string to mitigate timing attacks. |
required |
Likely non-resistant to timing attacks
This method makes some effort to be resistant to timing attacks, but cannot guarantee that Python micro-optimizations, version or platform differences affect the effectiveness of these efforts.
Callers can definitely observe timing differences due to the length of the passphrase passed in.