Skip to content

Recovery codes

Wladimir Palant edited this page May 3, 2023 · 3 revisions

Recovery codes encode a stored password in such a way that it can be printed safely. The password is encrypted and can only be extracted if the password protecting the recovery code (typically user’s main password) is known.

Binary data for V2 recovery codes

The raw data consists of the following fields:

Field Size in bits Value
version 8 2
algorithm 2 0 for Argon2d, 1 for Argon2i, 2 for Argon2id
algorithm_version 1 0 for version 0x10, 1 for version 0x13
parallelism_size 5 Size of the parallelism parameter
parallelism parallelism_size p parameter of the Argon2 algorithm
memory_size 5 Size of the memory parameter
memory memory_size m parameter of the Argon2 algorithm in MiB (not KiB)
iterations_size 5 Size of the iterations parameter
iterations iterations_size t parameter of the Argon2 algorithm
padding 0 to 7 Padding bits to byte-align the data
salt 128 Key derivation salt
iv 96 Random initialization vector for encryption
ciphertext Encrypted password

The encryption key is derived from the protection password using the designated Argon2 variant and parameters. This key and the random initialization vector are used to encrypt the password via the AES-GCM algorithm.

The password is padded with up to 13 NUL characters prior to encryption, making certain that the size of binary data is a multiple of 14.

Binary data for V1 (legacy) recovery codes

Note: This recovery code format was used by PfP 2.x. Starting with PfP 3.0, V2 recovery codes are used.

The raw data of a stored password consists of the following fields:

Field Size in bytes Value
version 1 1
salt 16 Salt used to derive the encryption key from the master password
iv 12 Random initialization vector for encryption
ciphertext Encrypted password

Password is padded with NUL characters before encryption to ensure that the size of binary data is a multiple of 14 bytes. With AES-GCM the size of ciphertext always increases by 16 bytes (tag size) compared to plaintext size, so shorter passwords will be NUL-padded towards 11 characters (total data size 56 bytes) whereas longer ones will be NUL-padded towards 25 characters (total data size 70 bytes).

Human-readable representation

For the human-readable representation, the binary data is split up into blocks of 14 bytes. For each block a Pearson hash is generated to detect input errors, single typos are guaranteed to be detected. This algorithm requires a substitution table which is built as follows:

for i from 0 to 255
    T[i] = (i + 379) * 467 % 256

The hashing function is always applied to 15 bytes of data. Here the first byte is the zero-based block index for regular blocks and 255 - block index for the last block. The other 14 bytes are the actual block contents. The result are 15 bytes where the first 14 bytes are the block contents and the last byte is the calculated hash value.

Finally, these 15 bytes are encoded as 24 base32 characters. The base32 alphabet used here is ABCDEFGHJKLMNPQRSTUVWXYZ23456789, some characters have been removed to avoid ambiguities when the recovery code is printed out (1 vs. I, 0 vs. O). These 24 characters form a row, with separators added for better readability:

ABCD-EFGH-JKLM:NPQR-STUV-WXYZ

Typically, a V1 recovery code will consist of 4 or 5 such rows, a V2 recovery code of 5 or 6 rows.