authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2024-11-22 10:02:14+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2024-11-22 10:02:14+01:00
log73dcd1914071984c5a2e7c195212404824dbfb9e
treec462d742c62749c18f4e2f594e96de11e9a7de2e
parent636308a17d8f8118ab34e9d4b217baa5878416c4
signaturebadge-check Signed by PGP key B5690EEEBB952194

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.

1 files changed, 55 insertions(+), 3 deletions(-)

lib/std/crypto/bcrypt.zig+55-3
...@@ -563,15 +563,57 @@ const pbkdf_prf = struct {...@@ -563,15 +563,57 @@ const pbkdf_prf = struct {
563};563};
564564
565/// bcrypt-pbkdf is a key derivation function based on bcrypt.565/// bcrypt-pbkdf is a key derivation function based on bcrypt.
566/// This is the function used in OpenSSH to derive encryption keys from passphrases.
567///
568/// This implementation is compatible with the OpenBSD implementation (https://github.com/openbsd/src/blob/master/lib/libutil/bcrypt_pbkdf.c).
569///566///
570/// Unlike the password hashing function `bcrypt`, this function doesn't silently truncate passwords longer than 72 bytes.567/// Unlike the password hashing function `bcrypt`, this function doesn't silently truncate passwords longer than 72 bytes.
571pub fn pbkdf(pass: []const u8, salt: []const u8, key: []u8, rounds: u32) !void {568pub fn pbkdf(pass: []const u8, salt: []const u8, key: []u8, rounds: u32) !void {
572 try crypto.pwhash.pbkdf2(key, pass, salt, rounds, pbkdf_prf);569 try crypto.pwhash.pbkdf2(key, pass, salt, rounds, pbkdf_prf);
573}570}
574571
572/// The function used in OpenSSH to derive encryption keys from passphrases.
573///
574/// This implementation is compatible with the OpenBSD implementation (https://github.com/openbsd/src/blob/master/lib/libutil/bcrypt_pbkdf.c).
575pub fn opensshKdf(pass: []const u8, salt: []const u8, key: []u8, rounds: u32) !void {
576 var tmp: [32]u8 = undefined;
577 var tmp2: [32]u8 = undefined;
578 if (rounds < 1 or pass.len == 0 or salt.len == 0 or key.len == 0 or key.len > tmp.len * tmp.len) {
579 return error.InvalidInput;
580 }
581 var sha2pass: [Sha512.digest_length]u8 = undefined;
582 Sha512.hash(pass, &sha2pass, .{});
583 const stride = (key.len + tmp.len - 1) / tmp.len;
584 var amt = (key.len + stride - 1) / stride;
585 if (math.shr(usize, key.len, 32) >= amt) {
586 return error.InvalidInput;
587 }
588 var key_remainder = key.len;
589 var count: u32 = 1;
590 while (key_remainder > 0) : (count += 1) {
591 var count_salt: [4]u8 = undefined;
592 std.mem.writeInt(u32, count_salt[0..], count, .big);
593 var sha2salt: [Sha512.digest_length]u8 = undefined;
594 var h = Sha512.init(.{});
595 h.update(salt);
596 h.update(&count_salt);
597 h.final(&sha2salt);
598 tmp2 = pbkdf_prf.hash(sha2pass, sha2salt);
599 tmp = tmp2;
600 for (1..rounds) |_| {
601 Sha512.hash(&tmp2, &sha2salt, .{});
602 tmp2 = pbkdf_prf.hash(sha2pass, sha2salt);
603 for (&tmp, tmp2) |*o, t| o.* ^= t;
604 }
605 amt = @min(amt, key_remainder);
606 key_remainder -= for (0..amt) |i| {
607 const dest = i * stride + (count - 1);
608 if (dest >= key.len) break i;
609 key[dest] = tmp[i];
610 } else amt;
611 }
612 crypto.secureZero(u8, &tmp);
613 crypto.secureZero(u8, &tmp2);
614 crypto.secureZero(u8, &sha2pass);
615}
616
575const crypt_format = struct {617const crypt_format = struct {
576 /// String prefix for bcrypt618 /// String prefix for bcrypt
577 pub const prefix = "$2";619 pub const prefix = "$2";
...@@ -847,3 +889,13 @@ test "bcrypt phc format" {...@@ -847,3 +889,13 @@ test "bcrypt phc format" {
847 verify_options,889 verify_options,
848 );890 );
849}891}
892
893test "openssh kdf" {
894 var key: [100]u8 = undefined;
895 const pass = "password";
896 const salt = "salt";
897 const rounds = 5;
898 try opensshKdf(pass, salt, &key, rounds);
899 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 };
900 try testing.expectEqualSlices(u8, &key, &expected);
901}