From 73dcd1914071984c5a2e7c195212404824dbfb9e Mon Sep 17 00:00:00 2001 From: Frank Denis <124872+jedisct1@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:02:14 +0100 Subject: [PATCH] std.crypto.bcrypt: implement the actual OpenSSH KDF (#22027) They way OpenSSH does key derivation to protect keys using a password is not the standard PBKDF2, but something funky, picking key material non-linearly. --- lib/std/crypto/bcrypt.zig | 58 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/lib/std/crypto/bcrypt.zig b/lib/std/crypto/bcrypt.zig index f3c30ab5ce..308cd1a42e 100644 --- a/lib/std/crypto/bcrypt.zig +++ b/lib/std/crypto/bcrypt.zig @@ -563,15 +563,57 @@ const pbkdf_prf = struct { }; /// bcrypt-pbkdf is a key derivation function based on bcrypt. -/// This is the function used in OpenSSH to derive encryption keys from passphrases. -/// -/// This implementation is compatible with the OpenBSD implementation (https://github.com/openbsd/src/blob/master/lib/libutil/bcrypt_pbkdf.c). /// /// Unlike the password hashing function `bcrypt`, this function doesn't silently truncate passwords longer than 72 bytes. pub fn pbkdf(pass: []const u8, salt: []const u8, key: []u8, rounds: u32) !void { try crypto.pwhash.pbkdf2(key, pass, salt, rounds, pbkdf_prf); } +/// The function used in OpenSSH to derive encryption keys from passphrases. +/// +/// This implementation is compatible with the OpenBSD implementation (https://github.com/openbsd/src/blob/master/lib/libutil/bcrypt_pbkdf.c). +pub fn opensshKdf(pass: []const u8, salt: []const u8, key: []u8, rounds: u32) !void { + var tmp: [32]u8 = undefined; + var tmp2: [32]u8 = undefined; + if (rounds < 1 or pass.len == 0 or salt.len == 0 or key.len == 0 or key.len > tmp.len * tmp.len) { + return error.InvalidInput; + } + var sha2pass: [Sha512.digest_length]u8 = undefined; + Sha512.hash(pass, &sha2pass, .{}); + const stride = (key.len + tmp.len - 1) / tmp.len; + var amt = (key.len + stride - 1) / stride; + if (math.shr(usize, key.len, 32) >= amt) { + return error.InvalidInput; + } + var key_remainder = key.len; + var count: u32 = 1; + while (key_remainder > 0) : (count += 1) { + var count_salt: [4]u8 = undefined; + std.mem.writeInt(u32, count_salt[0..], count, .big); + var sha2salt: [Sha512.digest_length]u8 = undefined; + var h = Sha512.init(.{}); + h.update(salt); + h.update(&count_salt); + h.final(&sha2salt); + tmp2 = pbkdf_prf.hash(sha2pass, sha2salt); + tmp = tmp2; + for (1..rounds) |_| { + Sha512.hash(&tmp2, &sha2salt, .{}); + tmp2 = pbkdf_prf.hash(sha2pass, sha2salt); + for (&tmp, tmp2) |*o, t| o.* ^= t; + } + amt = @min(amt, key_remainder); + key_remainder -= for (0..amt) |i| { + const dest = i * stride + (count - 1); + if (dest >= key.len) break i; + key[dest] = tmp[i]; + } else amt; + } + crypto.secureZero(u8, &tmp); + crypto.secureZero(u8, &tmp2); + crypto.secureZero(u8, &sha2pass); +} + const crypt_format = struct { /// String prefix for bcrypt pub const prefix = "$2"; @@ -847,3 +889,13 @@ test "bcrypt phc format" { verify_options, ); } + +test "openssh kdf" { + var key: [100]u8 = undefined; + const pass = "password"; + const salt = "salt"; + const rounds = 5; + try opensshKdf(pass, salt, &key, rounds); + const expected = [_]u8{ 65, 207, 68, 58, 55, 252, 114, 141, 255, 65, 216, 175, 5, 92, 235, 68, 220, 92, 118, 161, 40, 13, 241, 190, 56, 152, 69, 136, 41, 214, 51, 205, 37, 221, 101, 59, 105, 73, 133, 36, 14, 59, 94, 212, 111, 107, 109, 237, 213, 235, 246, 119, 59, 76, 45, 130, 142, 81, 178, 231, 161, 158, 138, 108, 18, 162, 26, 50, 218, 251, 23, 66, 2, 232, 20, 202, 216, 46, 12, 250, 247, 246, 252, 23, 155, 74, 77, 195, 120, 113, 57, 88, 126, 81, 9, 249, 72, 18, 208, 160 }; + try testing.expectEqualSlices(u8, &key, &expected); +}