nx_pbkdf2_sha1.nx source
↩ module page · 121 lines · 3907 B
1// pbkdf2_sha1.nx -- PBKDF2 key derivation with HMAC-SHA1.
2//
3// RFC 2898 / PKCS#5 v2.0. The SHA-1-based variant was once the
4// web standard for password hashing and is still used by:
5// - WPA2 PSK -> PMK derivation (802.11i-2004 requires PBKDF2-SHA1
6// with 4096 iterations)
7// - Older OS X Keychain + iOS data protection
8// - 1Password .agilekeychain / .keychain vault decryption
9// - Legacy JWT \"PBES2\" JOSE key derivation
10//
11// For new password hashing use Argon2id (memory-hard) or at least
12// PBKDF2-SHA-256 with iter_count >= 600000 (OWASP 2023).
13//
14// Algorithm (ยง5.2):
15// DK = T_1 || T_2 || ... || T_l
16// T_i = F(password, salt, c, i)
17// F(P, S, c, i) = U_1 XOR U_2 XOR ... XOR U_c
18// U_1 = HMAC-SHA1(P, S || INT(i)) INT(i) is u32 big-endian
19// U_j = HMAC-SHA1(P, U_{j-1}) for j = 2..c
20//
21// Composes hmac_sha1.nx.
22//
23// Invariants:
24// PB1 Output length `dk_len` in bytes; at most 2^32 - 1 blocks
25// (not checked; callers requesting > 10MB keys are daft).
26// PB2 iterations must be >= 1; we treat 0 as \"no stretching\"
27// which RFC doesn't define.
28// PB3 Salt + password byte-exact; no UTF-8 normalisation.
29
30// nx_safety_envelope:
31// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
32// sil_target: SIL1
33// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
34// verdict: NOT_YET_EVALUATED
35
36import "nx_syscalls.nx"
37import "nx_hmac_sha1.nx"
38
39const PB_HLEN: i64 = 20 // HMAC-SHA1 output size
40
41// Compute one output block T_i into `out[0..20]`.
42func pb_f_sha1(password: *u8, pass_len: i64,
43 salt: *u8, salt_len: i64,
44 iterations: i64, block_idx: i64,
45 out: *u8) -> i64 {
46 // U_1 = HMAC(P, S || INT(i))
47 let u_buf_len: i64 = salt_len + 4
48 let u_buf: *u8 = sys_mmap(u_buf_len + 16)
49 var i: i64 = 0
50 while i < salt_len {
51 u_buf[i] = salt[i]
52 i = i + 1
53 }
54 u_buf[salt_len] = (block_idx >> 24) & 0xFF
55 u_buf[salt_len + 1] = (block_idx >> 16) & 0xFF
56 u_buf[salt_len + 2] = (block_idx >> 8) & 0xFF
57 u_buf[salt_len + 3] = block_idx & 0xFF
58
59 let u: *u8 = sys_mmap(32)
60 hmac_sha1(password, pass_len, u_buf, u_buf_len, u)
61
62 // Accumulator starts as U_1.
63 i = 0
64 while i < PB_HLEN {
65 out[i] = u[i]
66 i = i + 1
67 }
68
69 // Iterate.
70 var j: i64 = 1
71 let u_next: *u8 = sys_mmap(32)
72 while j < iterations {
73 hmac_sha1(password, pass_len, u, PB_HLEN, u_next)
74 i = 0
75 while i < PB_HLEN {
76 u[i] = u_next[i]
77 out[i] = out[i] ^ u_next[i]
78 i = i + 1
79 }
80 j = j + 1
81 }
82 return 0
83}
84
85// Derive a key. Writes dk_len bytes to out.
86func pbkdf2_sha1(password: *u8, pass_len: i64,
87 salt: *u8, salt_len: i64,
88 iterations: i64,
89 out: *u8, dk_len: i64) -> i64 {
90 let block_buf: *u8 = sys_mmap(32)
91 var pos: i64 = 0
92 var block_idx: i64 = 1
93 while pos < dk_len {
94 pb_f_sha1(password, pass_len, salt, salt_len,
95 iterations, block_idx, block_buf)
96 var take: i64 = PB_HLEN
97 let remaining: i64 = dk_len - pos
98 if remaining < take { take = remaining }
99 var k: i64 = 0
100 while k < take {
101 out[pos + k] = block_buf[k]
102 k = k + 1
103 }
104 pos = pos + take
105 block_idx = block_idx + 1
106 }
107 return 0
108}
109
110// Compile-only smoke. RFC 6070 Test Vector 1:
111// P = "password", S = "salt", c = 1, dkLen = 20
112// DK = 0c60c80f961f0e71f3a9b524af6012062fe037a6
113func main() -> i64 {
114 let out: *u8 = sys_mmap(32)
115 pbkdf2_sha1("password", 8, "salt", 4, 1, out, 20)
116 if out[0] != 0x0C { return 1 }
117 if out[1] != 0x60 { return 2 }
118 if out[2] != 0xC8 { return 3 }
119 if out[19] != 0xA6 { return 4 }
120 return 0
121}