pbkdf2.nx source
↩ module page · 117 lines · 4386 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
33import "syscalls.nx"
34import "nx_hmac.nx" // was hmac.nx -- CODE-IDENTICAL twin (49/49 stmts) on the LEGACY syscalls.nx+sha256.nx family.
35// Two files defining hmac_sha256 + main, with the expander deduping BY PATH NOT BY SYMBOL, made
36// every legacy importer a duplicate-symbol landmine for the nx_ family (debt 1785524913).
37
38const PBKDF2_HASH_LEN: i64 = 32 // HMAC-SHA-256 output size
39
40func pbkdf2_sha256(password: *u8, pass_len: i64,
41 salt: *u8, salt_len: i64,
42 iterations: i64,
43 dk_len: i64,
44 dk_out: *u8) -> i64 {
45 if iterations < 1 { return -1 }
46 if dk_len < 1 { return -1 }
47
48 // Number of blocks needed.
49 let l: i64 = (dk_len + PBKDF2_HASH_LEN - 1) / PBKDF2_HASH_LEN
50
51 // Scratch for the salt || counter input to the first HMAC.
52 let salt_ctr: *u8 = sys_mmap(salt_len + 4 + 16)
53 var i: i64 = 0
54 while i < salt_len { salt_ctr[i] = salt[i]; i = i + 1 }
55
56 let u_curr: *u8 = sys_mmap(PBKDF2_HASH_LEN)
57 let u_prev: *u8 = sys_mmap(PBKDF2_HASH_LEN)
58 let t: *u8 = sys_mmap(PBKDF2_HASH_LEN)
59
60 var block: i64 = 1
61 while block <= l {
62 // Big-endian u32 counter appended to salt.
63 salt_ctr[salt_len + 0] = (block >> 24) & 0xFF
64 salt_ctr[salt_len + 1] = (block >> 16) & 0xFF
65 salt_ctr[salt_len + 2] = (block >> 8) & 0xFF
66 salt_ctr[salt_len + 3] = block & 0xFF
67
68 // U_1 = HMAC(password, salt || counter).
69 hmac_sha256(password, pass_len, salt_ctr, salt_len + 4, u_curr)
70 // Initialise T_j = U_1.
71 var k: i64 = 0
72 while k < PBKDF2_HASH_LEN {
73 t[k] = u_curr[k]
74 u_prev[k] = u_curr[k]
75 k = k + 1
76 }
77 // U_2..U_iterations.
78 var iter: i64 = 2
79 while iter <= iterations {
80 hmac_sha256(password, pass_len, u_prev, PBKDF2_HASH_LEN, u_curr)
81 k = 0
82 while k < PBKDF2_HASH_LEN {
83 t[k] = t[k] ^ u_curr[k]
84 u_prev[k] = u_curr[k]
85 k = k + 1
86 }
87 iter = iter + 1
88 }
89
90 // Copy T_j into output at position (block-1) * HASH_LEN.
91 let out_off: i64 = (block - 1) * PBKDF2_HASH_LEN
92 k = 0
93 while k < PBKDF2_HASH_LEN {
94 let dk_pos: i64 = out_off + k
95 if dk_pos < dk_len {
96 dk_out[dk_pos] = t[k]
97 }
98 k = k + 1
99 }
100 block = block + 1
101 }
102 return 0
103}
104
105// Compile-only smoke. RFC 6070 vector 3:
106// password = "password", salt = "salt", iterations = 4096,
107// dkLen = 20
108// -> 4b007901b765489abead49d926f721d065a429c1
109func main() -> i64 {
110 let pw: *u8 = "password"
111 let salt: *u8 = "salt"
112 let dk: *u8 = sys_mmap(32)
113 // Use a LOW iteration count in smoke tests so compile stays
114 // fast; real callers use OWASP-minimum ~600000 for SHA-256.
115 pbkdf2_sha256(pw, 8, salt, 4, 4, 20, dk)
116 return dk[0] as i64
117}