armour/doc/d/keyfile-0.md
Ari Archer 78bd42d697
Improve Keyfile intergrity.
Signed-off-by: Ari Archer <ari@ari.lt>
2025-01-04 20:23:56 +02:00

15 KiB

Keyfile -- version 0 (alpha)

Note: This version of pKf is currently in review pre-implementation. Reviews are welcome!

This document defines the format of a pDB Keyfile file version 0 (pKfv0), which is used to store various keys, their parameters, and public encryption parameters.

The purpose of this format is to define a format where keys may be stored in a non-raw format, adding a layer of authentication, authenticity, and authorization to the access of them. This system highly depends on the strength of your password, meaning you shall set a strong password - clients may force users to set a strong password to not compromise the security of the Keyfile system.

This file format has multiple passes of encryption with a single algorithm, which is good enough for obscuring the keys stored inside, although it is not recommended to share or spread your Keyfile publicly. It must be kept secret, and if publicly released, the system shall be classified as highly compromised.

File identifiers

  • File extension: .pkf
  • MIME type: application/pkf, application/x-pkf
  • Magic number: pdKf (0x70 0x64 0x4b 0x66, 0x70644b66, 1885621094)

Format

All multi-byte types (anything above uint8_t (so uint16_t, uint32_t, uint64_t, ...)) are little-endian values.

C type Name Error correction Description
uint8_t[4] magic The magic number of the file. Always a constant value.
uint16_t version The version of the Keyfile. A constant value per-version. (in the case of pKfv0 case - 0x00)
uint8_t[704] salt Reed Solomon (RS) with nsym=64 and nsize=255. The cryptographically secure Keyfile salt.
uint16_t db_AES_crypto_passes When using AES256 cryptography in GCM (Galois/Counter) mode in the database, how many times should the algorithm me ran?
uint16_t db_ChaCha20_Poly1305_crypto_passes When using ChaCha20-Poly1305 cryptography in the database, how many times should the algorithm me ran?
uint64_t sum Parity sum of db_AES_crypto_passes and db_ChaCha20_Poly1305_crypto_passes.
uint8_t[32] xxh3_parity RS with nsym=16 and nsize=255. XXH3-128 hash of the elements that go into sum.
uint8_t[128] db_pepper RS with nsym=64 and nsize=255. 512 bits of cryptographically secure information which are always constant. Used for peppering of data in the database.
uint8_t[128] header_sha3_512_sum RS with nsym=64 and nsize=255. The SHA3-512 hash of the header before the hash.
uint8_t[128] sha3_512_sum RS with nsym=64 and nsize=255. The SHA3-512 hash of the keys database after the hash. (note: excluding the lock)
uint8_t lock Is the Keyfile currently locked/locking/...? See lock statuses below. (support for concurrency)
uint64_t lock_token A unique token identifying the lock holder. (discussed below)
uint8_t[] keys The keys and/or their parameters stored in the Keyfile. Dynamic section of encrypted chunks.

A generic layout of everything would look like this:

[magic][0x00][locked][sha3-512][salt] (header)
[type][size][encrypted key]... (the keys)
(Raw: [type][size][provision date][lifetime][salt][...]...)
(For instance: [0x00][size][provision date][lifetime][salt][public key size][public key][IV][key][tag][secret key]...)

Please note that Keyfile depends on pDB database for these parameters:

  • Argon2_type
  • Argon2_time_cost
  • Argon2_memory_cost
  • psalt

While the database depends on all parameters with the db_ prefix, so:

  • db_AES_crypto_passes
  • db_ChaCha20_Poly1305_crypto_passes
  • db_pepper

Do not be confused when you see those parameters in this document, assume they come from the pDB database.

Error correction helper sum

sum is a sum of:

  • db_AES_crypto_passes
  • db_ChaCha20_Poly1305_crypto_passes

And is only used when trying to recover the database from corruption.

The brute-force success is checked by passing the same parameters, in order, to xxHash3-128 (an extremely fast non-cryptographic hashing function) and checking whether or not the output hash matches xxh3_parity. If it does, then it is checked against header_sha3_512_sum, and if it is passing - it is sane to assume that errors have been corrected. Else, the brute-force continues.

Lock status

  • 0x00: Unlocked.
  • 0x01: Locked.
  • 0x02: Disabled. Consult the database. (Forever locked, lock handled by a client service)
    • Normal lock resolution process is executed on the database (including SNAPI resolution).
  • Anything else: Invalid.

Lock token

A lock token is any unique 64-bit value that identifies the current lock holder. The standard way to derive this would be to just generate a cryptographically secure random value.

Keys format

The keys are a dynamic section of encrypted chunks. Every block is dynamic and the keys do not have an infinite lifetime, a key may last up to 255 days. The format is as follows:

C type Name Error correction Description
uint8_t[128] sha3_512_sum RS with nsym=64 and nsize=255. SHA3-512 sum of the whole key (type, size, parity, and data).
uint8_t type The type of the key. (see types below)
uint32_t size The size of the data blob after decoding it using RS.
uint64_t sum Sum of of type and size.
uint8_t[32] xxh3_parity RS with nsym=16 and nsize=255. XXH3-128 hash of type and size.
uint8_t[calc_rs_size(size)] data RS with nsym=64 and nsize=255. The encrypted data of the key.

The keys are in order, IDs should be assigned from ID 0, 0 being the key at the beginning of file, IDs are of type uint64_t, although not stored, so can be pretty much any type, it is just very unrealistic that there will ever be more than 18446744073709551615 keys in the database, or 3074457345618258432 rounds of pDBv1 provisioning (276701161105643258880 days on average, or 758085372892173312 years).

The encryption of data is discussed below. After the blob was encrypted it may be appended to the Keyfile.

Key types

This section describes the formats for differing key formats defined by the key section. All keys are encrypted and timestamped. Keys always have these fields before the actual data:

(All multi-byte types (anything above uint8_t (so uint16_t, uint32_t, uint64_t, ...)) are little-endian values.)

C type Name Description
uint64_t provision_timestamp The date of key creation in UNIX UTC time, in seconds.
uint8_t lifetime Lifetime of the key in days, if zero - instant expiry.
uint8_t[128] salt 1024-bit key salt.

(Formula to check the expiration status: (current_timestamp - provision_timestamp) > (lifetime * 24 * 60 * 60) , where current_timestamp is the current (as time of accessing provision_timestamp) UTC UNIX time timestamp)

Followed by one of the following formats, based off the type:

0x00 - RSA-4096 key pair

This is the format of an RSA-4096 public and secret key pair:

(All multi-byte types (anything above uint8_t (so uint16_t, uint32_t, uint64_t, ...)) are little-endian values.)

C type Name Description
uint16_t pk_size Public key size.
uint8_t[pk_size] pk Public key (DER format).
uint8_t[] sk Secret key (DER format) encrypted using a single pass of ChaCha20-Poly1305.

Encryption of the secret key would look like this:

bytes encrypt_sk(sk) {
    bytes assoc = random(32);
    bytes nonce = random(12);

    bytes key = argon2(password=(database_password + nonce), salt=assoc, length=32, ... (parameters configured by database));

    ChaCha20Poly1305 chacha = ChaCha20Poly1305(key=key);

    # Encrypt the secret key
    bytes ciphertext = chacha.encrypt(data=sk, nonce=nonce, associated_data=assoc);

    return assoc + nonce + ciphertext;
}

This pseudocode means:

  • Generate 32 bytes of cryptographically secure associated data.
  • Generate a 12-byte cryptographically secure nonce for ChaCha20-Poly1305.
  • Derive a key using Argon2, password being the database password and the nonce concatenated, and the salt being the associated data.
  • Pass in the key to ChaCha20-Poly1305.
  • Encrypt the secret key, passing in the nonce and the associated data
  • Concatenate the associated data, the nonce, and the cypher-text, and return it as the final cypher-text.

0x01 - cryptographic salt

This is the format of a cryptographic salt:

C type Name Description
uint8_t[] value A cryptographically secure salt (random bytes).

0x02 - account secret

This is the format of a cryptographic account secret for SNAPI.

C type Name Description
uint8_t[] value A secure account secret (random bytes).

Cryptography

Keyfile version 0 uses ChaCha20-Poly1305 with the Argon2 key derivation function. In pseudocode, the cryptography of a single key would look like this:

bytes encrypt_key(key, key_salt) {
    # `salt` comes from the format header
    bytes database_password_digest = argon2(password=(database_password + psalt), salt=(salt + key_salt), length=256, ... (parameters configured by database));

    for _ in repeat(keyfile_crypto_passes) {
        bytes ks = random(32);

        bytes assoc = random(32);

        bytes nonce = argon2(password=(db_pepper + database_password + assoc), salt=(ks + database_password_digest + key_salt), length=12, ...);

        bytes key = argon2(password=(nonce + database_password + assoc), salt=(database_password_digest + key_salt + db_pepper), length=32, ...);

        ChaCha20Poly1305 chacha = ChaCha20Poly1305(key=key);

        key = chacha.encrypt(data=key, nonce=nonce, associated_data=assoc);
        key = ks + assoc + key;
    }

    return key;
}

In other words:

  • Initially a 256-byte database password digest is derived using Argon2, passing in the database password and psalt (configured by the database) as the password, and the Keyfile salt and key salt as the salt.
  • A loop of keyfile_crypto_passes is started (configured by the database).
  • A 32-byte cryptographically secure salt is generated called ks.
  • 32 bytes of associative data called assoc is generated to be later passed to ChaCha20-Poly1305.
  • Using Argon2 a 12-byte nonce is derived, by passing in db_pepper, database password, and the previously generated assoc concatenated as the password, and the salt being ks, database password digest, and the key salt concatenated together.
  • Using Argon2, a 32-byte key is derived. Password: nonce + database password + assoc, Salt: database password digest + key_salt + db_pepper.
  • Key is passed to ChaCha20.
  • Using ChaCha20, data is encrypted. Key is reassigned to be the cypher-text.
  • ks + assoc concatenation is prepended to the key cypher-text.
  • Process is repeated.

Validation

  • The magic number of the file is correct. (basic corruption and file type check)
  • The version is supported by the target database. (support check)
  • The Keyfile is not currently locked. (access check, to prevent collisions)
  • db_AES_crypto_passes is at least 1.
  • db_ChaCha20_Poly1305_crypto_passes is at least 1.
  • The SHA3-512 sum of the header is correct. (integrity check)
  • The SHA3-512 sum of the database is correct. (integrity check)
  • All keys are decryptable and valid. (integrity, authentication, and authorization checks (because a password, correct nonce and associated data, and correct cypher-text is required))
  • The provision date of any key must not be into the future.

If any of the checks fail, you shall terminate the access to the database to prevent any damage or tampered with data.

Authors

Licensing

"pDB Keyfile version 0 (pKfv0) file format and specification" is licensed under the GNU General Public License version 3 or later (GPL-3.0-or-later).

You should have received a copy of the GNU General Public License along with this program.
If not, see <https://www.gnu.org/licenses/>.