nx_pbkdf2.nx source
↩ module page · 135 lines · 5319 B
1// pbkdf2.nx -- PBKDF2 password-based key derivation (RFC 8018).
2//
3// Stretches a user password (low-entropy) into a crypto key
4// (high-entropy) by iterating HMAC many times over (password,
5// salt, counter). The iteration count makes brute-force attacks
6// expensive while legitimate derivations pay only a one-time cost.
7//
8// Used for: user-auth password hashing, key wrapping, PKCS#5
9// v2 PBKDF2 (widely deployed in 1Password / KeePass / LastPass
10// container formats, TLS-PSK).
11//
12// Superseded for new systems by Argon2id (memory-hard;
13// harder to attack with GPUs). Still required for interop with
14// many existing databases + hardware tokens.
15//
16// Algorithm (RFC 8018 ยง5.2 PBKDF2):
17// For each `dkLen / hash_len` block:
18// U_1 = HMAC(password, salt || counter_as_u32_be)
19// U_i = HMAC(password, U_{i-1}) for i = 2..iterations
20// T_j = U_1 XOR U_2 XOR ... XOR U_iterations
21// Concatenate T_j for the final derived key.
22//
23// Invariants:
24// P1 iterations must be >= 1. OWASP-minimum for SHA-256 is
25// ~600_000 as of 2023. Caller chooses; we don't enforce.
26// P2 dkLen capped at (2^32 - 1) * hash_len. In practice nothing
27// needs more than a handful of blocks; we don't validate.
28// P3 Counter is 1-indexed, big-endian, appended to salt for the
29// U_1 computation.
30// P4 XOR accumulation is in-place to avoid allocating a per-
31// iteration scratch.
32//
33// nx_safety_envelope:
34// intended_use: "PBKDF2 password-based key derivation
35// (RFC 2898 / NIST SP 800-132) -- legacy
36// password hashing; prefer Argon2 for new code"
37// sil_target: SIL3 (password hash; iteration count
38// determines brute-force cost)
39// asil_target: QM
40// dal_target: DAL B
41// evidence: [RFC_2898_canonical_basis,
42// NIST_SP_800-132_iteration_count_guidance,
43// constant_time_HMAC_inheritance]
44// hazard_register: [bug-tape-iteration-count-too-low,
45// bug-tape-salt-too-short-or-reused,
46// bug-tape-side-channel-via-non-CT-compare]
47// residual_risk: "Iteration count is caller-supplied;
48// OWASP recommends >= 600000 for SHA-256
49// as of 2023. Substrate cannot enforce.
50// New code SHOULD use Argon2 (queued)."
51// verdict: NOT_YET_EVALUATED
52
53import "nx_syscalls.nx"
54import "nx_hmac.nx"
55
56const PBKDF2_HASH_LEN: i64 = 32 // HMAC-SHA-256 output size
57
58func pbkdf2_sha256(password: *u8, pass_len: i64,
59 salt: *u8, salt_len: i64,
60 iterations: i64,
61 dk_len: i64,
62 dk_out: *u8) -> i64 {
63 if iterations < 1 { return -1 }
64 if dk_len < 1 { return -1 }
65
66 // Number of blocks needed.
67 let l: i64 = (dk_len + PBKDF2_HASH_LEN - 1) / PBKDF2_HASH_LEN
68
69 // Scratch for the salt || counter input to the first HMAC.
70 let salt_ctr: *u8 = sys_mmap(salt_len + 4 + 16)
71 var i: i64 = 0
72 while i < salt_len { salt_ctr[i] = salt[i]; i = i + 1 }
73
74 let u_curr: *u8 = sys_mmap(PBKDF2_HASH_LEN)
75 let u_prev: *u8 = sys_mmap(PBKDF2_HASH_LEN)
76 let t: *u8 = sys_mmap(PBKDF2_HASH_LEN)
77
78 var block: i64 = 1
79 while block <= l {
80 // Big-endian u32 counter appended to salt.
81 salt_ctr[salt_len + 0] = (block >> 24) & 0xFF
82 salt_ctr[salt_len + 1] = (block >> 16) & 0xFF
83 salt_ctr[salt_len + 2] = (block >> 8) & 0xFF
84 salt_ctr[salt_len + 3] = block & 0xFF
85
86 // U_1 = HMAC(password, salt || counter).
87 hmac_sha256(password, pass_len, salt_ctr, salt_len + 4, u_curr)
88 // Initialise T_j = U_1.
89 var k: i64 = 0
90 while k < PBKDF2_HASH_LEN {
91 t[k] = u_curr[k]
92 u_prev[k] = u_curr[k]
93 k = k + 1
94 }
95 // U_2..U_iterations.
96 var iter: i64 = 2
97 while iter <= iterations {
98 hmac_sha256(password, pass_len, u_prev, PBKDF2_HASH_LEN, u_curr)
99 k = 0
100 while k < PBKDF2_HASH_LEN {
101 t[k] = t[k] ^ u_curr[k]
102 u_prev[k] = u_curr[k]
103 k = k + 1
104 }
105 iter = iter + 1
106 }
107
108 // Copy T_j into output at position (block-1) * HASH_LEN.
109 let out_off: i64 = (block - 1) * PBKDF2_HASH_LEN
110 k = 0
111 while k < PBKDF2_HASH_LEN {
112 let dk_pos: i64 = out_off + k
113 if dk_pos < dk_len {
114 dk_out[dk_pos] = t[k]
115 }
116 k = k + 1
117 }
118 block = block + 1
119 }
120 return 0
121}
122
123// Compile-only smoke. RFC 6070 vector 3:
124// password = "password", salt = "salt", iterations = 4096,
125// dkLen = 20
126// -> 4b007901b765489abead49d926f721d065a429c1
127func main() -> i64 {
128 let pw: *u8 = "password"
129 let salt: *u8 = "salt"
130 let dk: *u8 = sys_mmap(32)
131 // Use a LOW iteration count in smoke tests so compile stays
132 // fast; real callers use OWASP-minimum ~600000 for SHA-256.
133 pbkdf2_sha256(pw, 8, salt, 4, 4, 20, dk)
134 return dk[0] as i64
135}