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

240 lines
15 KiB
Markdown

# 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
- Ari Archer \<<ari@ari.lt>\> \[<https://ari.lt/>\]
## 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/>.