mnemonikey

package module
v0.0.0-...-a929568 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 19, 2022 License: MIT Imports: 16 Imported by: 1

README

mnemonikey Unit Tests

Deterministic backup and recovery of PGP keys using human-readable phrases.

Save your PGP identity as a list of 15 English words. Use these words to recover lost keys or derive new subkeys.

Generation Recovery
generate recover

Mnemonikey allows you to back up your PGP keys without managing highly sensitive and awkward digital files, without any loss of security.

Mnemonikey deterministically derives a full set of PGP keys based on a secure, randomly generated seed. That seed (and the key creation timestamp) is then re-exported in the form of an English phrase which you can record on paper to fully back up your PGP key. The recovery phrase is encoded similarly to how Bitcoin wallets are backed up, and even uses the same word list for better cross-compatibility (although the number of words in a Mnemonikey recovery phrase is different).

🚨 ⚠ WARNING ⚠ 🚨

The Mnemonikey specification is NOT YET FINALIZED.

Until this warning is removed, DO NOT use Mnemonikey to generate PGP keys for real-world use.

Please see the discussions board to help with finalizing the Mnemonikey specification.

PGP Key Backup Alternatives

Backup Format Secure By Default Memorizable Offline Robust
Secret Key File on a hard drive or SD card
passphrase2pgp
paperkey ✅ *
mnemonikey

* paperkey printouts are only as secure as the printer used to print them

Installation

To use the mnemonikey command-line interface utility, first install Golang, and then run:

$ go install github.com/kklash/mnemonikey/cmd/mnemonikey@latest

To use mnemonikey as a Golang library:

$ go get -u github.com/kklash/mnemonikey

Specification 📜

Here follows a specification which would allow anyone to re-implement mnemonikey in another language.


Definitions

Word Definition
PGP Key Set A set of four PGP keys: the master key for certification, and three subkeys for signing, encryption, and authentication.
Mnemonikey Epoch Midnight in UTC time on new year's eve between 2021 and 2022 (AKA 1640995200 in unix time). This is used to identify the absolute time encoded by the key creation offset.
Key Creation Offset The number of seconds after the mnemonikey epoch when the PGP key set was created. This offset is serialized as a 30-bit unsigned integer in the backup payload.
Seed A securely-generated random integer, containing 128 bits of entropy.
Backup Payload The combination of a seed and key creation offset. Together, they form a backup payload, which can be used to fully recover a PGP key set
Recovery Phrase / Mnemonic Phrase A sequence of 15 English words which encodes the backup payload and checksum.
Checksum Generator Polynomial The CRC-32 IEEE generator polynomial. Used for checksumming the backup payload. ( $x^{32} + x^{26} + x^{23} + x^{22} + x^{16} + x^{12} + x^{11} + x^{10} + x^{8} + x^{7} + x^{5} + x^{4} + x^{2} + x + 1$ )

Goals

Mnemonikey is primarily concerned with defining two high-level procedures:

  • Key Derivation: Deterministically deriving a usable PGP key set from a seed and key creation time.
  • Mnemonic Encoding: Encoding a seed and key creation time into a mnemonic phrase for later decoding & recovery.

Dependencies

Dependency Usage
The BIP39 English Word List Encodes the backup payload into a form that is easy to write down on paper, or remember mnemonically. Each word is one of 2048 potential words, thus encodes 11 bits of information.
HMAC-based Key Derivation Function (HKDF) Stretches the seed into suitable PGP private keys.
ED25519 (EdDSA) Algorithm used for the certification master key, and for the authentication and signing subkeys. Used to create signatures to bind the PGP key set together.
Curve25519 (ECDH) Generates the encryption subkey.
CRC (cyclic redundancy check) Used for checksumming the backup payload.

Key Derivation

Inputs
Input Description Required
version A 2-bit version number. The current latest version number is 0. YES
seed A securely-generated random integer with 128 bits of entropy. YES
creationOffset The number of seconds after the Mnemonikey Epoch when the key was created. Represented by a 30-bit unsigned integer. YES
name A human-readable display name used to build the PGP key's user-identifier string. NO
email An email address used to build the PGP key's user-identifier string. NO
ttl A time-to-live duration in seconds applied to the output PGP key set. NO
password A byte-array used to symmetrically encrypt the output PGP key set. NO
encSubkeyIndex A 16-bit integer which can be used to derive different encryption subkeys. NO
authSubkeyIndex A 16-bit integer which can be used to derive different authentication subkeys. NO
sigSubkeyIndex A 16-bit integer which can be used to derive different signing subkeys. NO
Output

A serialized PGP private key set, containing a certification-enabled ED25519 master key, along with properly bound signing, encryption, and authentication subkeys.

  • If password was provided, the output PGP key set's private keys will be encrypted symmetrically using OpenPGP's String-to-Key (S2K) protocol.
  • If ttl was provided, the output PGP key set's keys will be set to expire at ttl seconds after the key creation time.
  • If name and/or email was provided, they form the key's user-identifier (UID).

The keys will have been deterministically generated by the inputs.

Procedure
  1. If the version number is greater than 0, fail with an unsupported version error.
  2. Determine the exact key creation timestamp based on creationOffset by interpreting creationOffset as the number of seconds after the Mnemonikey Epoch.
    • creation = creationOffset + 1640995200
  3. Derive the master key and subkeys from the seed using HDKF-Expand.
    • To derive the ED25519 master certification key, read 32 bytes from HDKF-Expand on seed using SHA256 with the string "mnemonikey master key" as the info parameter.
    • For each of the subkeys, the HKDF-Expand info parameter is built as follows:
      • Begin with the string "mnemonikey TYPE subkey", where TYPE is replaced by any of "encryption", "authentication", or "signing" depending on the type of subkey.
      • Serialize and append a 16-bit big-endian subkey index parameter for the subkey (encSubkeyIndex for the encryption subkey, etc).
      • For example, to derive the encryption subkey at index 3: info = b"mnemonikey encryption subkey\x00\x03"
    • To derive the Curve25519 ECDH encryption subkey, read 32 bytes from HDKF-Expand on seed using SHA256, with the info parameter built as described above.
    • To derive the ED25519 authentication subkey, read 32 bytes from HDKF-Expand on seed using SHA256, with the info parameter built as described above.
    • To derive the ED25519 signing subkey, read 32 bytes from HDKF-Expand on seed using SHA256, with the info parameter built as described above.
  4. Derive the ED25519 and Curve25519 public keys for the master key and subkeys.
    • The exact cryptographic procedure for public-key derivation and signatures is out-of-scope for this specification. See the original Curve25519 paper and the original ED25519 paper by Daniel Bernstein and friends.
    • Most modern languages will have libraries available to perform this cryptography for you.
  5. Build a PGP user ID (UID) string.
    • If name is defined, and email is not, set uid = name.
    • If email is defined, and name is not, set uid = email.
    • If both name and email are defined, set uid = name + " <" + email + ">".
  6. Use the master certification key to create a positive self-certification signature on the master key, committing to uid and creation.
    • If ttl is defined, use it to define the master key expiry time creation + ttl, and commit the self-certification signature to that expiry.
    • Refer to the OpenPGP packet format specification to see how this signature should be computed.
    • This signature should also commit to the symmetric cipher algorithm AES-256, and the hash algorithm SHA-256, as keyholder preferences.
    • Call this certificationSig.
  7. Use the master certification key to create binding signatures on each subkey, committing to creation.
    • If ttl is defined, commit the binding signatures to creation + ttl as the subkey expiry time.
    • Refer to the OpenPGP packet format specification to see how these signatures should be computed.
    • Call these encryptionBindSig, authenticationBindSig, and signingBindSig for the encryption, authentication, and signing subkeys respectively.
  8. Serialize and return the following as OpenPGP packets:

The resulting serialized binary blob is a usable OpenPGP private key, deterministically derived from the seed. It can be encoded to ASCII Armor format as an OpenPGP Private Key block for easy copy-pasting, or imported directly into an OpenPGP implementation like gpg.

PGP Considerations and Parameters
  • PGP keys and signature packets must be encoded with the OpenPGP version 4 format.
  • Key derivation parameters should be set to SHA-256 and AES-256.
    • SHA-256 and AES-256 are also set as the hash and cipher preferences in the self-certification signature. This is not mandatory, but reduces the risk that software into which the key is imported will use outdated defaults like SHA1 and Tripe-DES, by providing a safe default set of algorithms which is usually available for most software. To overwrite these default preferences with your own in gpg, use gpg --edit-key and type setpref.
  • The master key must be configured with the Certify flag. Its algorithm must be set to EDDSA (22).
  • The encryption subkey must be configured with Encrypt-Communications (0x04) and Encrypt-Storage (0x08) flags. Its algorithm must be set to ECDH (18).
  • The authentication subkey must be configured with the Authentication (0x20) flag. Its algorithm must be set to EDDSA (22).
  • The signing subkey must be configured with the Signing (0x02) flag. Its algorithm must be set to EDDSA (22).
  • The self-certification signature must use a positive-certification signature type (0x13).
  • The subkey binding signatures must use type 0x18.
  • Signatures should use SHA256 as the hashing function.
  • Signature timestamps should be the same as the key creation timestamp to ensure signatures are deterministic, regardless of when keys are recovered.
  • If the caller wishes to only export a subset of the keys, for example to generate a new subkey and export it without also exposing the master key, the master key can be encoded as a private key stub using the GNU-Dummy S2K extension. No official docs seem to be available for this, however, as it is not an official part of the OpenPGP specification.

Mnemonic Encoding

Inputs
Input Description Required
version A 2-bit version number. The current latest version number is 0. YES
seed A securely-generated random integer with 128 bits of entropy. YES
creationOffset The number of seconds after the Mnemonikey Epoch when the key was created. Represented by a 30-bit integer. YES
Output

An English phrase of 15 words which completely encodes the version, seed, and creationOffset.

Procedure
  1. Initialize an unsigned integer backupPayload starting with the 2-bit version number.
    • backupPayload = version
  2. Append the 128-bit integer seed to backupPayload as the lowest-order bits.
    • backupPayload = (backupPayload << 128) | seed
  3. Append the 30-bit integer creationOffset to backupPayload as the lowest-order bits.
    • backupPayload = (backupPayload << 30) | creationOffset
  4. Serialize the backupPayload (now 160-bits long) as a big-endian byte array with length 20.
    • Call the result backupPayloadBytes
    • backupPayloadBytes = backupPayload.to_bytes(20, 'big')
  5. Compute a 5-bit checksum on backupPayloadBytes using a cyclic-redundancy-check (CRC).
    • Use the Checksum Generator Polynomial to generate a 32-bit CRC.
    • Take the lowest order 5 bits of the CRC output.
    • checksum = 0x1F & crc32(backupPayloadBytes)
    • Example: The 5-bit checksum of "hey" in UTF-8 would be 0x1F & crc32([0x68, 0x65, 0x79]) = 16
  6. Append checksum to the lowest order bits of backupPayload.
    • backupPayload = (backupPayload << 5) | checksum
  7. Chunk backupPayload into 11-bit symbols, with the highest-order bits as the first chunk.
    • [(backupPayload >> (i*11)) & 0x7FF for i in reversed(range(15))]
    • Note that the total backupPayload bit-length $2 + 128 + 30 + 5 = 165$ is evenly divisible by 11.
  8. Interpret each 11-bit symbol as a big-endian integer, mapping to the word at that index in the BIP39 word list.
  9. Return the resulting list of words.

To decode a mnemonic phrase into the version, seed, and creationOffset, simply reverse the algorithm. Upon decoding, a Mnemonikey implementation should ensure the checksum is correct and the version number is supported.

Design Motivation 🧠

In this section, we elaborate on the motivation behind the different design choices implied by the above specification.

Background

Normally, PGP key backups must be done manually by backing up private key export files, but files can be easily lost, corrupted, or deleted accidentally. Whenever new subkeys are added to the master key, the backup must be updated manually. This is a risky and error-prone practice, as I have personally discovered several times.

The paperkey backup tool is also similarly fragile, as it involves either copying by-hand a large blob of hexadecimal numbers, or printing paperkey's output to paper. Copying by-hand risks errors in duplication, and printing risks exposure of your private key material to a printer, which are not known for prioritizing security in their design: Many printers run outdated firmware, are wide-open by default on wireless networks, and may cache plaintext copies of the documents they've printed, in memory or on-disk.

Many early users of Bitcoin also learned this lesson in a different context. In 2013, mnemonic recovery phrases were invented to resolve it. They eventually became the norm for Bitcoin wallet backups.

Bitcoin Recovery Phrase paperkey Output

On the left, a 12-word Bitcoin BIP39 recovery phrase written on paper. On the right, the output of paperkey. Which would you rather use to recover the root of your digital identity?

Mission

The purpose of Mnemonikey is to securely and deterministically generate a PGP key, and export a backup which contains the minimum amount of information needed to repeat the key generation process. This backup can be used to recover the PGP key without adverse effects, much in the same way a BIP39 recovery phrase allows the bearer to re-derive the private keys in a cryptocurrency wallet.

Creation Timestamps

OpenPGP keys are identified by their fingerprint, which is a hash of the serialized public key, including its creation time. That means in order to make deterministic key backup and recovery possible without invalidating the previous key's signatures, the creation time must either be encoded into the backup, or it must be a static constant used for all keys. We chose to encode the creation time into the backup, because having distinct key creation times is convenient and worth the extra few bits of information lengthening the backup.

Key creation times in OpenPGP keys are represented as 32-bit unix timestamps. We can save a bit more space by reducing the size of the key creation timestamp slightly. We don't need a full 32-bits of precision, since a very large chunk of time since the unix epoch on 1970-01-01 has already passed, and Mnemonikey was invented in 2022. If we define our own epoch, we can easily trim down to 30 bits of precision, while retaining perfect second-level accuracy, and supporting key creation times until 2056-01-10. Hopefully by then, people will have adopted more advanced cryptography tools than PGP. If Mnemonikey is still in use by then, we can simply bump the version number and redefine the creation timestamp encoding rules, for example by defining a new epoch.

We could reduce the size of the key creation timestamp much more by sacrificing timestamp granularity. If we were OK with per-day accuracy only, we could reduce the creation offset integer's size to 15-bits of precision, but this comes at the cost of privacy: Keys created with such granularity in their timestamp could be easily identified as having come from Mnemonikey.

Timestamp size Words Needed Ceiling (1s granularity) Ceiling (1min granularity) Ceiling (1h granularity) Ceiling (1d granularity)
32 bits 15 words 2158-02-06 Very High Very High Very High
31 bits 15 words 2090-01-18 6105-01-23 Very High Very High
30 bits 15 words 2056-01-10 4063-07-13 Very High Very High
29 bits 15 words 2039-01-05 3042-10-07 Very High Very High
28 bits 15 words 2030-07-04 2532-05-20 Very High Very High
27 bits 15 words 2026-04-03 2277-03-11 Very High Very High
26 bits 15 words 2024-02-16 2149-08-06 9677-09-28 Very High
25 bits 15 words Too Low 2085-10-18 5849-11-14 Very High
24 bits 14 words Too Low 2053-11-24 3935-12-09 Very High
23 bits 14 words Too Low 2037-12-13 2978-12-19 Very High
22 bits 14 words Too Low 2029-12-22 2500-06-26 Very High
21 bits 14 words Too Low 2025-12-27 2261-03-30 7763-10-22
20 bits 14 words Too Low Too Low 2141-08-15 4892-11-25
19 bits 14 words Too Low Too Low 2081-10-23 3457-06-14
18 bits 14 words Too Low Too Low 2051-11-27 2739-09-23
17 bits 14 words Too Low Too Low 2036-12-13 2380-11-11
16 bits 14 words Too Low Too Low 2029-06-23 2201-06-07
15 bits 14 words Too Low Too Low 2025-09-27 2111-09-19
14 bits 14 words Too Low Too Low Too Low 2066-11-09
13 bits 13 words Too Low Too Low Too Low 2044-06-05
12 bits 13 words Too Low Too Low Too Low 2033-03-19
11 bits 13 words Too Low Too Low Too Low 2027-08-10
10 bits 13 words Too Low Too Low Too Low 2024-10-20
9 bits 13 words Too Low Too Low Too Low Too Low
8 bits 13 words Too Low Too Low Too Low Too Low
Script to generate the above table
#!/usr/bin/env python3

from datetime import datetime

column_names = [
  "Timestamp size",
  "Words Needed",
  "Ceiling (1s granularity)",
  "Ceiling (1min granularity)",
  "Ceiling (1h granularity)",
  "Ceiling (1d granularity)",
]

granularities = [1, 60, 60**2, 60**2 * 24]

epoch = 1640995200

rows = []

for ts_bits in reversed(range(8, 33)):
  n_words = (128 + ts_bits + 2 + 10) // 11

  row = ["%d bits" % ts_bits, "%d words" % n_words]

  for g in granularities:
    try:
      d = datetime.fromtimestamp(epoch + (1 << ts_bits)*g - 1)
      if d.year <= 2023:
        ceiling = "Too Low"
      else:
        ceiling = "`" + str(d.date()) + "`"
    except:
      ceiling = "Very High"
    row.append(ceiling)
  rows.append("|" + '|'.join(row) + "|")

header = "|" + "|".join(column_names) + "|"
delim =  "|" + "|".join([":" + "-" * (len(name)-2) + ":" for name in column_names]) + "|"

print('\n'.join([header, delim] + rows))

We might have chosen to require the user to provide a copy of the original public key when recovering, since an OpenPGP public key packet would also contain the key creation timestamp. This is the approach used by paperkey. However, it may be counterintuitive to the average PGP user to require the public key to recover a private key, so instead we keep a minimum of recovery information bundled in the recovery phrase, and restrict the generation process tightly so as to enforce determinism when the key is re-generated later. This choice also simplifies Mnemonikey implementations, because they do not need to be concerned with parsing OpenPGP key packets, only generating them.

Curve Choice

We chose ED25519 as the elliptic curve for the output PGP keys because:

  • Its keys are very small.
  • It has excellent performance.
  • It is designed to avoid common security pitfalls in downstream implementations.
  • It is designed with 'nothing up my sleeve' parameters to reduce risk of a back-door.
  • It is usable for signing, encryption, and authentication with SSH.
  • Golang has first-class ED25519 and Curve25519 implementations available.

Subkey Lifecycle

To allow cycling of subkeys, Mnemonikey can derive each type of subkey at any arbitrary index from 0 to 65535 (0xFFFF). Subkeys are always derived in parallel from the seed and not from one-another. The only way to derive a new subkey is to use the seed embedded in the recovery phrase.

No subkey has any special preference or power over any other. When generated, any subkeys derived from the same seed will all have the same creation timestamp as the master key.

The master key cannot be used to re-derive any Mnemonikey subkeys, although it could be used to sign new non-deterministically generated subkeys out-of-band and bind them to the same user ID. We recommend not to do this, because any subkeys generated outside of Mnemonikey cannot be re-generated with the Mnemonikey recovery phrase later.

The normal use of subkeys involves revoking subkeys once they become compromised or outdated. To do this with Mnemonikey, one would revoke their old subkey at index $n$ using a downstream PGP tool (gpg --edit uid and type key n, and then type revkey). Then Mnemonikey can be used to generate a fresh subkey at index $n+1$. This derived subkey should be exported on its own, orphaned from the master key and other subkeys, and imported into the PGP keychain.

Example

Derive a new signing subkey at index 1 with the mnemonikey CLI, and import it into a GPG keychain which already contains the related master key.

$ mnemonikey recover -only signing -sig-index 1 -self-cert=false | gpg --import
Generate a master key and signing subkey (starts at default index 0).
Derive the new signing subkey at index 1, and revoke the old one at index 0.

-self-cert=false is an optional flag which tells Mnemonikey not to output the master key's self-certification signature on the user ID. This is useful when minting new subkeys, in a situation where you already have the master key stored safely in your keyring. It prevents adding unneeded extra certification signatures to your keychain.

Security

To derive the full set of four OpenPGP keys - the master key and the signing/encryption/authentication subkeys - we need a total of $256 \cdot 4 = 1024$ bits of secure key material. For this we use the HMAC-based Key Derivation Function (HKDF) Expand function to expand a uniformly random seed into four 256-bit private keys.

The Edwards 25519 curve has a security level of 128 bits for its public keys. Using less than 128 bits of seed entropy would make keys more susceptible to brute-force attacks through guessing the seed than by guessing the key. Using more than 128 bits of seed entropy to generate ED25519 keys doesn't add extra security to any one key, and would increase the size of the recovery phrase making it more difficult to write down or memorize. Thus, we use 128 bits as the quantity of entropy needed to create the full set of ED25519 OpenPGP keys.

Note that because we only use one seed to generate many keys, it follows that anyone with the intent of brute-force attacking someone's PGP key would find more success in brute-force guessing the seed than in attempting to attack any of the PGP public keys available to them, because reversing a public key into a private key would only yield success for one of the keys, whereas correctly guessing the seed would be tantamount to a successful attack on the entire set of keys all at once.

For PGP keys this concern is not terribly relevant. If Eve wishes to attack Bob's PGP identity, she need only compute his master certification key, as this is the root of PGP trust. Eve could then impersonate Bob by revoking keys and issuing new ones as needed. If Eve could perform a batch-attack on all of the subkeys and the master key at once, that would seem to be a minor improvement at best over an independent attack on the master certification key.

Encoding

Each word in a recovery phrase is one of 2048 ( $2^{11}$ ) different words, and thus encodes 11 bits of information. To encode a recovery phrase, we need to store both the entropy (128 bits) and the key creation time offset from the epoch (30 bits). $128 + 30 = 158$ and $\frac{158}{11} = 14\frac{4}{11}$. Therefore, the minimum number of words we could use to encode the full backup payload would be 15 words, leaving 7 unused bits.

These last 7 bits can be used for a 2-bit version number and 5-bit checksum.

This diagram demonstrates the bit-level layout of each word in the mnemonic, and how it encodes the backup payload and checksum. Each number from 0 to a (10) is an index of each of the 11 bits encoded by a word in the mnemonic.

version
 |---|----------------------------------- entropy ----------------------------------------
 |------- word 0 ------|------- word 1 ------|------- word 2 ------|------- word 3 ------|
  0 1 2 3 4 5 6 7 8 9 a 0 1 2 3 4 5 6 7 8 9 a 0 1 2 3 4 5 6 7 8 9 a 0 1 2 3 4 5 6 7 8 9 a


 ---------------------------------------- entropy ----------------------------------------
 |------- word 4 ------|------- word 5 ------|------- word 6 ------|------- word 7 ------|
  0 1 2 3 4 5 6 7 8 9 a 0 1 2 3 4 5 6 7 8 9 a 0 1 2 3 4 5 6 7 8 9 a 0 1 2 3 4 5 6 7 8 9 a


 ---------------------------------------- entropy ----------------------------------------
 |------- word 8 ------|------- word 9 ------|------- word 10 -----|------- word 11------|
  0 1 2 3 4 5 6 7 8 9 a 0 1 2 3 4 5 6 7 8 9 a 0 1 2 3 4 5 6 7 8 9 a 0 1 2 3 4 5 6 7 8 9 a


 ---- entropy -----|-------------------- creation timestamp -------------------|check-sum|
 |------- word 12 -----|------- word 13 -----|------- word 14 -----|------- word 15 -----|
  0 1 2 3 4 5 6 7 8 9 a 0 1 2 3 4 5 6 7 8 9 a 0 1 2 3 4 5 6 7 8 9 a 0 1 2 3 4 5 6 7 8 9 a

Version Number

The version number tells Mnemonikey implementations how to decode the recovery phrase and derive the PGP keys from the seed entropy. The version number may be incremented in the future, for example to fix a critical bug, support post-quantum key algorithms, or define a new key creation time epoch.

Checksum

Words in a recovery phrase must already be members of the BIP39 wordlist to be considered valid, but on the off-chance a user enters another valid but incorrect word, a 5-bit checksum will provide error detection with a collision probability of roughly $\frac{1}{32}$. With a word list of 2048 valid words, that means there are only 64 possible collision-causing 1-word substitutions which the user could accidentally perform for each word in the phrase.

We chose to use 32-bit cyclic-redundancy-checks (CRC-32) with the IEEE generator polynomial as our checksum, because of its speed and ubiquitous availability. There are cross-compatible implementations available in the standard libraries of most programming languages, and numerous test vectors available should anyone need to re-implement it.

Acknowledgments

Thanks to Chris Wellons (@skeeto) and his awesome tool, passphrase2pgp for inspiring me, and serving as a helpful reference for how PGP keys are serialized into packets.

Thanks to fellow PGP nerd Ryan Zimmerman (@ryanzim) for jamming with me to draft the specification.

Donations

If you're interested in supporting development of this package, show your love by dropping me some bitcoins!

bc1qhct3hwt5pjmu75d2fldwd477vhwmthuqvmh03s

Documentation

Index

Constants

View Source
const (
	// VersionLatest is the latest known mnemonikey version number. Backups encoded with versions higher
	// than this number will fail to decode.
	VersionLatest uint = 0

	// VersionBitCount is the number of bits in the backup payload reserved for the version number.
	VersionBitCount uint = 2

	// CreationOffsetBitCount is the number of bits used to represent a key creation offset.
	CreationOffsetBitCount uint = 30

	// ChecksumBitCount is the number of bits in the backup payload reserved for the checksum.
	ChecksumBitCount uint = 5

	// EntropyBitCount is the number of bits of entropy in the seed used to derive PGP keys.
	EntropyBitCount = 128

	// MnemonicSize is the number of mnemonic words needed to encode
	// both the key creation offset and 128 bits of seed entropy.
	MnemonicSize uint = 15
)

checksumTable is the precomputed table used to create checksums of backup payloads.

View Source
const EpochIncrement = time.Second

EpochIncrement is the level of granularity available for the creation date of keys generated by mnemonikey.

Variables

View Source
var EpochStart = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)

EpochStart is the start of the epoch after which key creation times are encoded in recovery phrases. It is exactly midnight in UTC time on the new year's eve between 2021 and 2022.

In unix time, this epoch is exactly 1640995200 seconds after the unix epoch.

View Source
var ErrCreationTooEarly = errors.New("key creation time exceeds maximum")

ErrCreationTooEarly is returned when constructing a Mnemonikey, if its creation time is before EpochStart.

View Source
var ErrCreationTooLate = errors.New("key creation time exceeds maximum")

ErrCreationTooLate is returned when constructing a Mnemonikey, if its creation time is too far in the future to fit in CreationOffsetBitCount.

View Source
var ErrInvalidChecksum = errors.New("failed to validate checksum embedded in mnemonic phrase")

ErrInvalidChecksum is returned when decoding a mnemonic fails due to a checksum mismatch.

View Source
var ErrInvalidWordCount = fmt.Errorf("mnemonics must be %d words long", MnemonicSize)

ErrInvalidWordCount is returned when decoding a mnemonic recovery phrase whose word count is not MnemonicSize.

View Source
var ErrTTLInvalid = errors.New("key time-to-live cannot be negative")

ErrTTLInvalid is returned when constructing a Mnemonikey, if the TTL selected is less than zero.

View Source
var ErrUnsupportedSeedVersion = errors.New("unsupported seed version number")

ErrUnsupportedSeedVersion is returned when an unknown or outdated version number was used - For example, this can occur when recovering or decoding a mnemonikey backup phrase exported by a newer version of mnemonikey.

View Source
var MaxCreationTime = EpochStart.Add(EpochIncrement * (time.Duration(1<<CreationOffsetBitCount) - 1))

MaxCreationTime is the farthest point in the future that the mnemonikey recovery phrase encoding algorithm can represent key creation timestamps for.

Functions

func ParseVersion

func ParseVersion(firstWord string) (version uint, err error)

ParseVersion parses the version number encoded in first word of the recovery phrase. Returns mnemonic.ErrInvalidWord if the word is not in the word list. Returns ErrUnsupportedSeedVersion if the version number is greater than VersionLatest.

This can be used to inform a user ahead of time if the user's mnemonic recovery phrase is not supported by this version of the Mnemonikey library, thus saving them from entering the whole phrase in needlessly.

Types

type KeyOptions

type KeyOptions struct {
	// Name and Email are used to build the user ID for a PGP key.
	Name, Email string

	// TTL defines the validity period of the generated key. This TTL is relative to the
	// key creation time, so that keys derived using the same options and seed will always
	// be deterministic. TTL must be a positive duration.
	TTL time.Duration

	// These are indices which allow the caller to choose a specific subkey to generate.
	// Each subkey at different indices is completely independent of each other - there is
	// no hierarchy.
	EncryptionSubkeyIndex     uint16
	AuthenticationSubkeyIndex uint16
	SigningSubkeyIndex        uint16

	// Subkeys is a list of the types of subkeys to derive. If this list is nil, a full
	// set of three subkeys will be generated for encryption, authentication, and signing.
	Subkeys []SubkeyType
}

KeyOptions are a set of optional parameters which can be supplied when generating or recovering Mnemonikeys. They affect specific parameters in the output PGP keys but are not needed to recover the private keys themselves.

type Mnemonikey

type Mnemonikey struct {
	// contains filtered or unexported fields
}

Mnemonikey represents a determinstically generated set of PGP keys. It contains a master certification key, and encryption, authentication, and signing subkeys, as well as the seed data used to derive all four keys.

func New

func New(seed *Seed, creation time.Time, opts *KeyOptions) (*Mnemonikey, error)

New constructs a Mnemonikey from a seed.

The key creation timestamp is hashed when computing the PGP public key fingerprint, and thus is critical to ensuring deterministic key re-generation. This function rounds the creation time down to the most recent EpochIncrement before creation, so that it can be encoded into a recovery mnemonic.

The user ID parameters, name and email, are not required but are highly recommended to assist in identifying the key later.

func Recover

func Recover(words []string, opts *KeyOptions) (*Mnemonikey, error)

Recover decodes a seed and creation offset from the given recovery mnemonic and re-derives its child PGP keys.

The given name and email must be the same as was used to originally generate the key, otherwise the key fingerprint will not match.

If the original key's user ID is not a standard RFC-2822 mail name-addr format (NAME <EMAIL>), then simply provide the entire user ID as the name parameter, and leave the email parameter empty.

func (*Mnemonikey) CreatedAt

func (mnk *Mnemonikey) CreatedAt() time.Time

CreatedAt returns the key creation timestamp, rounded to an EpochIncrement after the EpochStart date.

func (*Mnemonikey) EncodeMnemonic

func (mnk *Mnemonikey) EncodeMnemonic() ([]string, error)

EncodeMnemonic encodes the Mnemonikey seed and creation offset into an English recovery mnemonic. The recovery mnemonic alone is sufficient to recover the entire set of keys.

func (*Mnemonikey) EncodePGP

func (mnk *Mnemonikey) EncodePGP(password []byte) ([]byte, error)

EncodePGP encodes the entire Mnemonikey as a series of binary OpenPGP packets.

If password is provided, it is used to encrypt private key material with the OpenPGP String-to-Key algorithm.

func (*Mnemonikey) EncodePGPArmor

func (mnk *Mnemonikey) EncodePGPArmor(password []byte) (string, error)

EncodePGPArmor encodes the entire Mnemonikey as a series of OpenPGP packets and formats them to ASCII armor block format.

If password is provided, it is used to encrypt private key material with the OpenPGP String-to-Key algorithm.

func (*Mnemonikey) EncodeSubkeysPGP

func (mnk *Mnemonikey) EncodeSubkeysPGP(password []byte, withSelfCert bool) ([]byte, error)

EncodeSubkeysPGP encodes the Mnemonikey as a series of binary OpenPGP packets, but only includes the private key material for subkeys. The master key is encoded as a private key stub without providing the private key material itself.

If withSelfCert is false, the self-certification signature by the master key will not be provided. Typically the self-certification is required when first importing a set of PGP keys into a keychain, and can be skipped if importing into a keychain which already has a self-certification signature recorded for the key. Leave withSelfCert as true if you are unsure.

If password is provided, it is used to encrypt private key material with the OpenPGP String-to-Key algorithm.

func (*Mnemonikey) EncodeSubkeysPGPArmor

func (mnk *Mnemonikey) EncodeSubkeysPGPArmor(password []byte, withSelfCert bool) (string, error)

EncodeSubkeysPGPArmor encodes the Mnemonikey as a series of OpenPGP packets formatted to ASCII armor block format, but only includes the private key material for subkeys. The master key is encoded as a private key stub without providing the private key material itself.

If withSelfCert is false, the self-certification signature by the master key will not be provided. Typically the self-certification is required when first importing a set of PGP keys into a keychain, and can be skipped if importing into a keychain which already has a self-certification signature recorded for the key. Leave withSelfCert as true if you are unsure.

If password is provided, it is used to encrypt private key material with the OpenPGP String-to-Key algorithm.

func (*Mnemonikey) Expiry

func (mnk *Mnemonikey) Expiry() time.Time

Expiry returns the key expiry time.

func (*Mnemonikey) FingerprintV4

func (mnk *Mnemonikey) FingerprintV4() []byte

FingerprintV4 returns the SHA1 hash of the master key and the key user ID.

func (*Mnemonikey) SubkeyFingerprintV4

func (mnk *Mnemonikey) SubkeyFingerprintV4(subkeyType SubkeyType) []byte

SubkeyFingerprintV4 returns the SHA1 hash of the given subkey type. Returns nil if the Mnemonikey was created without the given subkey.

func (*Mnemonikey) SubkeyTypes

func (mnk *Mnemonikey) SubkeyTypes() []SubkeyType

SubkeyTypes returns the types of subkeys available to the Mnemonikey.

func (*Mnemonikey) UserID

func (mnk *Mnemonikey) UserID() string

UserID returns the string form of the key's user ID.

type Seed

type Seed struct {
	Version uint
	Value   *big.Int
}

Seed represents a seed which was generated with a specific number of bits of entropy. Each Seed contains a version number which indicates how the seed should be used to derive PGP keys.

Byte-representations of that seed should always be a fixed size, regardless of the actual integer value of the seed.

func DecodeMnemonic

func DecodeMnemonic(words []string) (seed *Seed, creation time.Time, err error)

DecodeMnemonic decodes a recovery mnemonic into the embedded Seed data and key creation timestamp.

func GenerateSeed

func GenerateSeed(random io.Reader) (*Seed, error)

GenerateSeed generates a random Seed using the given random source.

The generated seed will use VersionLatest.

func NewSeed

func NewSeed(version uint, entropyInt *big.Int) (*Seed, error)

NewSeed constructs a Seed at a given version number using the provided entropy integer.

Returns ErrUnsupportedSeedVersion if the version number is greater than VersionLatest.

func (*Seed) Bytes

func (seed *Seed) Bytes() []byte

Bytes returns the big-endian byte representation of seed.Value.

type SubkeyType

type SubkeyType string

SubkeyType represents a flavor of subkey, either encryption, authentication, or signing.

const (
	SubkeyTypeEncryption     SubkeyType = "encryption"
	SubkeyTypeAuthentication SubkeyType = "authentication"
	SubkeyTypeSigning        SubkeyType = "signing"
)

Directories

Path Synopsis
cmd
mnemonikey module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL