core/crypto/pbkdf2: Initial import

This commit is contained in:
Yawning Angel
2024-02-26 11:01:18 +09:00
parent 2f2a92866b
commit 290168f862
4 changed files with 244 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
/*
package pbkdf2 implements the PBKDF2 password-based key derivation function.
See: https://www.rfc-editor.org/rfc/rfc2898
*/
package pbkdf2
import "core:crypto/hash"
import "core:crypto/hmac"
import "core:encoding/endian"
import "core:mem"
// derive invokes PBKDF2-HMAC with the specified hash algorithm, password,
// salt, iteration count, and outputs the derived key to dst.
derive :: proc(
hmac_hash: hash.Algorithm,
password: []byte,
salt: []byte,
iterations: u32,
dst: []byte,
) {
h_len := hash.DIGEST_SIZES[hmac_hash]
// 1. If dkLen > (2^32 - 1) * hLen, output "derived key too long"
// and stop.
dk_len := len(dst)
switch {
case dk_len == 0:
return
case u64(dk_len) > u64(max(u32)) * u64(h_len):
// This is so beyond anything that is practical or reasonable,
// so just panic instead of returning an error.
panic("crypto/pbkdf2: derived key too long")
case:
}
// 2. Let l be the number of hLen-octet blocks in the derived key,
// rounding up, and let r be the number of octets in the last block.
l := dk_len / h_len // Don't need to round up.
r := dk_len % h_len
// 3. For each block of the derived key apply the function F defined
// below to the password P, the salt S, the iteration count c, and
// the block index to compute the block.
//
// 4. Concatenate the blocks and extract the first dkLen octets to
// produce a derived key DK.
//
// 5. Output the derived key DK.
// Each iteration of F is always `PRF (P, ...)`, so instantiate the
// PRF, and clone since memcpy is faster than having to re-initialize
// HMAC repeatedly.
base: hmac.Context
defer hmac.reset(&base)
hmac.init(&base, hmac_hash, password)
// Process all of the blocks that will be written directly to dst.
dst_blk := dst
for i in 1 ..= l { // F expects i starting at 1.
_F(&base, salt, iterations, u32(i), dst_blk[:h_len])
dst_blk = dst_blk[h_len:]
}
// Instead of rounding l up, just proceass the one extra block iff
// r != 0.
if r > 0 {
tmp: [hash.MAX_DIGEST_SIZE]byte
blk := tmp[:h_len]
defer mem.zero_explicit(raw_data(blk), h_len)
_F(&base, salt, iterations, u32(l + 1), blk)
copy(dst_blk, blk)
}
}
@(private)
_F :: proc(base: ^hmac.Context, salt: []byte, c: u32, i: u32, dst_blk: []byte) {
h_len := len(dst_blk)
tmp: [hash.MAX_DIGEST_SIZE]byte
u := tmp[:h_len]
defer mem.zero_explicit(raw_data(u), h_len)
// F (P, S, c, i) = U_1 \xor U_2 \xor ... \xor U_c
//
// where
//
// U_1 = PRF (P, S || INT (i)) ,
// U_2 = PRF (P, U_1) ,
// ...
// U_c = PRF (P, U_{c-1}) .
//
// Here, INT (i) is a four-octet encoding of the integer i, most
// significant octet first.
prf: hmac.Context
// U_1: PRF (P, S || INT (i))
hmac.clone(&prf, base)
hmac.update(&prf, salt)
endian.unchecked_put_u32be(u, i) // Use u as scratch space.
hmac.update(&prf, u[:4])
hmac.final(&prf, u)
copy(dst_blk, u)
// U_2 ... U_c: U_n = PRF (P, U_(n-1))
for _ in 1 ..< c {
hmac.clone(&prf, base)
hmac.update(&prf, u)
hmac.final(&prf, u)
// XOR dst_blk and u.
for v, i in u {
dst_blk[i] ~= v
}
}
}

View File

@@ -33,6 +33,7 @@ import hmac "core:crypto/hmac"
import keccak "core:crypto/legacy/keccak"
import md5 "core:crypto/legacy/md5"
import sha1 "core:crypto/legacy/sha1"
import pbkdf2 "core:crypto/pbkdf2"
import poly1305 "core:crypto/poly1305"
import sha2 "core:crypto/sha2"
import sha3 "core:crypto/sha3"
@@ -149,6 +150,7 @@ _ :: chacha20poly1305
_ :: hmac
_ :: keccak
_ :: md5
_ :: pbkdf2
_ :: poly1305
_ :: sha1
_ :: sha2

View File

@@ -53,6 +53,7 @@ main :: proc() {
test_hash(&t)
test_mac(&t)
test_kdf(&t) // After hash/mac tests because those should pass first.
test_chacha20(&t)
test_chacha20poly1305(&t)

View File

@@ -0,0 +1,119 @@
package test_core_crypto
import "core:encoding/hex"
import "core:fmt"
import "core:testing"
import "core:crypto/hash"
import "core:crypto/pbkdf2"
@(test)
test_kdf :: proc(t: ^testing.T) {
log(t, "Testing KDFs")
test_pbkdf2(t)
}
@(test)
test_pbkdf2 :: proc(t: ^testing.T) {
log(t, "Testing PBKDF2")
tmp: [64]byte // 512-bits is enough for every output for now.
test_vectors := []struct {
algo: hash.Algorithm,
password: string,
salt: string,
iterations: u32,
dk: string,
} {
// SHA-1
// - https://www.rfc-editor.org/rfc/rfc2898
{
hash.Algorithm.Insecure_SHA1,
"password",
"salt",
1,
"0c60c80f961f0e71f3a9b524af6012062fe037a6",
},
{
hash.Algorithm.Insecure_SHA1,
"password",
"salt",
2,
"ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957",
},
{
hash.Algorithm.Insecure_SHA1,
"password",
"salt",
4096,
"4b007901b765489abead49d926f721d065a429c1",
},
// This passes but takes a about 8 seconds on a modern-ish system.
//
// {
// hash.Algorithm.Insecure_SHA1,
// "password",
// "salt",
// 16777216,
// "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984",
// },
{
hash.Algorithm.Insecure_SHA1,
"passwordPASSWORDpassword",
"saltSALTsaltSALTsaltSALTsaltSALTsalt",
4096,
"3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038",
},
{
hash.Algorithm.Insecure_SHA1,
"pass\x00word",
"sa\x00lt",
4096,
"56fa6aa75548099dcc37d7f03425e0c3",
},
// SHA-256
// - https://www.rfc-editor.org/rfc/rfc7914
{
hash.Algorithm.SHA256,
"passwd",
"salt",
1,
"55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783",
},
{
hash.Algorithm.SHA256,
"Password",
"NaCl",
80000,
"4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d",
},
}
for v, _ in test_vectors {
algo_name := hash.ALGORITHM_NAMES[v.algo]
dst := tmp[:len(v.dk) / 2]
password := transmute([]byte)(v.password)
salt := transmute([]byte)(v.salt)
pbkdf2.derive(v.algo, password, salt, v.iterations, dst)
dst_str := string(hex.encode(dst, context.temp_allocator))
expect(
t,
dst_str == v.dk,
fmt.tprintf(
"HMAC-%s: Expected: %s for input of (%s, %s, %d), but got %s instead",
algo_name,
v.dk,
v.password,
v.salt,
v.iterations,
dst_str,
),
)
}
}