Skip to content

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 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 ASCII printable characters except lowercase characters, uppercase characters, digits, space and 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.

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_hash(
    phrase: Buffer | str,
    service: Buffer | 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 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 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: Buffer | str,
    /,
    *,
    phrase: Buffer | str = b"",
) -> bytes

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 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

phrases_are_interchangable classmethod

phrases_are_interchangable(
    phrase1: Buffer, phrase2: Buffer
) -> bool

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.