Skip to content

Submodule vault

derivepassphrase.vault

Python port of the vault(1) password generation scheme.

Vault

Vault(
    *,
    phrase: bytes | bytearray | 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 bytes | bytearray | 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 lower, but for ASCII uppercase characters.

None
number int | None

Same as lower, but for ASCII digits.

None
space int | None

Same as lower, but for the space character.

None
dash int | None

Same as lower, but for the hyphen-minus and underscore characters.

None
symbol int | None

Same as lower, but for all other hitherto unlisted ASCII printable characters (except backquote).

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.

create_hash classmethod

create_hash(
    phrase: bytes | bytearray | str,
    service: bytes | bytearray | str,
    *,
    length: int = 32
) -> bytes

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 bytes | bytearray | 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 bytes | bytearray | str

A vault service name. Will be suffixed with Vault._UUID, and then used as the salt value for PBKDF2. If a string, then the UTF-8 encoding of the string is used.

required
length int

The length of the byte stream to generate.

32

Returns:

Type Description
bytes

A pseudorandom byte string of length 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(
    service_name: bytes | bytearray | str,
    /,
    *,
    phrase: bytes | bytearray | str = b"",
) -> bytes

Generate a service passphrase.

Parameters:

Name Type Description Default
service_name bytes | bytearray | str

The service name. If a string, then the UTF-8 encoding of the string is used.

required
phrase bytes | bytearray | 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: bytes | bytearray,
    /,
    *,
    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 bytes | bytearray

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: bytes | bytearray,
    /,
    *,
    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 bytes | bytearray

The (public) SSH key to use for signing.

required
conn SSHAgentClient | socket | None

An optional connection hint to the SSH agent. See ssh_agent.SSHAgentClient.ensure_agent_subcontext.

None

Returns:

Type Description
bytes

The signature of the vault UUID under this key, unframed but encoded in base64.

Raises:

Type Description
KeyError

conn was None, and the SSH_AUTH_SOCK environment variable was not found.

NotImplementedError

conn was None, and this Python does not support socket.AF_UNIX, so the SSH agent client cannot be automatically set up.

OSError

conn was a socket or None, and there was an error setting up a socket connection to the agent.

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