code wiki / (root) / nx_pbkdf2.nx

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}