kdf.rs

crates/veilvoice-crypto/src/kdf.rs

veilvoice-crypto · 633 lines · read the source here · or on GitHub

Password-based key derivation with Argon2id.

Argon2id is the memory-hard KDF recommended by RFC 9106 and the OWASP password-storage guidance; the id variant resists both GPU/ASIC parallelism and the side-channel exposure of pure Argon2i.

Parameters travel with the ciphertext rather than being compiled in, so a file encrypted today still opens after the defaults are raised, and a user on a small machine can lower the memory cost without forking the format.

Cost parameters arrive from a file, so they are hostile input

That flexibility has a sharp edge, and two shipped defects came from it. m_cost and p_cost are read verbatim from a .veil header -- and from the app-lock file, which is parsed before anyone has authenticated.

  • argon2 0.5.3 evaluates m_cost < p_cost * 8 before it checks whether p_cost is within range, so a large p_cost overflows the multiplication. With overflow checks on -- every debug build, and any project consuming this crate as a library -- that is a panic on attacker-controlled input (F-2).
  • m_cost is allocated before anything else happens, so a header claiming u32::MAX asks for 4 TiB. The allocation fails, and a failed allocation aborts the process. Merely opening a hostile container killed the program (F-3).

Both are bounded in KdfParams::checked, in arithmetic that cannot overflow. Never bypass that funnel. It is the single place every derivation passes through, and it exists because the alternative -- checks scattered across the call sites -- is how one of them gets missed.

A residual is stated rather than fixed: a container may still declare a legitimate-but-expensive cost, so an attacker can make opening their file slow. That is inherent to shipping the cost with the file, which is what lets old files open after defaults rise. Slow is not crashing, and the user chose to open that file.

Domain separation

The app-lock password and the recording passphrase are different secrets and are kept different: they are domain-separated in the derivation, so unlocking the application does not unseal recordings and one cannot be derived from the other.

In plain words

This turns a passphrase into a key.

A passphrase somebody can remember is far too short and too predictable to use directly, so it is put through a process designed to be slow and to need a large amount of memory. That does not slow you down noticeably once, but it makes guessing millions of passphrases enormously expensive for anybody trying.

The settings are stored with each file, so an old recording still opens after the defaults are made stronger.

WHAT THIS FILE CONTAINS

633 lines defining 7 functions (5 public), 1 type and 6 constants. Everything below is read out of the source, so it cannot disagree with the code.

The types it owns.

  • struct KdfParams line 63 · Argon2id cost parameters.

What happens when it runs. These are the ways in: public, and nothing else in this file calls them, so they are what an outside caller reaches first.

  • KdfParams::weak_for_tests line 90 · A deliberately cheap profile for tests and low-memory devices.
  • KdfParams::within line 196 · Check the costs against a caller-chosen memory ceiling as well as the built-in one.
    reaches checked
  • derive_key line 278 · Derive a 32-byte key from password and salt.
  • random_salt line 291 · Draw a fresh random salt from the OS CSPRNG.

WHAT CALLS WHAT

KdfParams::default line 77 KdfParams::weak_for_tests line 90 KdfParams::within line 196 KdfParams::checked line 238 KdfParams::build line 261 derive_key line 278 random_salt line 291 entry: a way in: public, and nothing in this file calls it api: public, and also used inside this file helper: private to this file dashed: a call that goes back up, or across a wrapped rank The functions this file defines, and the calls between them. An edge means the callee's name appears, called, inside the caller's body. This is a syntactic reading, not a type-resolved one.

The functions this file defines, and the calls between them. An edge means the callee's name appears, called, inside the caller's body. This is a syntactic reading, not a type-resolved one.

The same graph as Mermaid source
%%{init: {"theme":"base","themeVariables":{"background":"#1a1b26","primaryColor":"#1f2335","primaryTextColor":"#c0caf5","primaryBorderColor":"#7aa2f7","secondaryColor":"#16161e","tertiaryColor":"#16161e","lineColor":"#737aa2","textColor":"#c0caf5","mainBkg":"#1f2335","nodeBorder":"#7aa2f7","clusterBkg":"#16161e","clusterBorder":"#2f3549","fontFamily":"ui-monospace, SFMono-Regular, Consolas, monospace","fontSize":"14px"}}}%%
flowchart TD
    n_default["KdfParams::default<br/>line 77"]
    n_weak_for_tests(["KdfParams::weak_for_tests<br/>line 90"])
    n_within(["KdfParams::within<br/>line 196"])
    n_checked["KdfParams::checked<br/>line 238"]
    n_build["KdfParams::build<br/>line 261"]
    n_derive_key(["derive_key<br/>line 278"])
    n_random_salt(["random_salt<br/>line 291"])
    n_build --> n_checked
    n_within --> n_checked
    click n_default href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/kdf.rs#L77" "open the source"
    click n_weak_for_tests href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/kdf.rs#L90" "open the source"
    click n_within href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/kdf.rs#L196" "open the source"
    click n_checked href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/kdf.rs#L238" "open the source"
    click n_build href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/kdf.rs#L261" "open the source"
    click n_derive_key href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/kdf.rs#L278" "open the source"
    click n_random_salt href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/kdf.rs#L291" "open the source"
    classDef entry fill:#1f2335,stroke:#7aa2f7,color:#c0caf5
    class n_weak_for_tests,n_within,n_derive_key,n_random_salt entry
    classDef api fill:#1f2335,stroke:#7dcfff,color:#c0caf5
    class n_checked api
    classDef helper fill:#1f2335,stroke:#bb9af7,color:#c0caf5
    class n_default,n_build helper

This site loads no third-party script, so it cannot run Mermaid; the diagram above is the same nodes and edges drawn by the generator instead. GitHub renders the source below directly.

ITEMS

ItemLineDocumentation
KdfParams pub struct63Argon2id cost parameters.
KdfParams::default fn77RFC 9106's "first recommended" profile: 2 GiB is the second option, but 256 MiB with three passes is the sweet spot for an interactive desktop unlock: strong against offline cracking while still opening a file in well under a second on ordinary hardware.
KdfParams::weak_for_tests pub fn90A deliberately cheap profile for tests and low-memory devices.
KdfParams::MAX_P_COST const99Argon2's own documented ceiling on parallelism: 2^24 - 1.
KdfParams::MAX_M_COST pub const120The largest memory cost this build will attempt, in KiB, which is 4 GiB.
KdfParams::MAX_T_COST pub const160The largest number of passes this build will attempt.
KdfParams::UNATTENDED_MAX_M_COST pub const176A ceiling for a caller with nobody watching.
KdfParams::within pub fn196Check the costs against a caller-chosen memory ceiling as well as the built-in one.
KdfParams::checked pub fn238Check the costs are ones Argon2 can accept, before handing them to it.
KdfParams::build fn261Reject values Argon2 cannot accept, so a corrupt header fails loudly rather than panicking deep inside the KDF.
SALT_LEN pub const270Length of the salt stored in an encrypted container.
KEY_LEN pub const272Length of a derived symmetric key.
derive_key pub fn278Derive a 32-byte key from password and salt.
random_salt pub fn291Draw a fresh random salt from the OS CSPRNG.