From 290168f86209df04246095ddacc65ecba1ed7777 Mon Sep 17 00:00:00 2001 From: Yawning Angel Date: Mon, 26 Feb 2024 11:01:18 +0900 Subject: [PATCH] core/crypto/pbkdf2: Initial import --- core/crypto/pbkdf2/pbkdf2.odin | 122 ++++++++++++++++++++ examples/all/all_main.odin | 2 + tests/core/crypto/test_core_crypto.odin | 1 + tests/core/crypto/test_core_crypto_kdf.odin | 119 +++++++++++++++++++ 4 files changed, 244 insertions(+) create mode 100644 core/crypto/pbkdf2/pbkdf2.odin create mode 100644 tests/core/crypto/test_core_crypto_kdf.odin diff --git a/core/crypto/pbkdf2/pbkdf2.odin b/core/crypto/pbkdf2/pbkdf2.odin new file mode 100644 index 000000000..20e490135 --- /dev/null +++ b/core/crypto/pbkdf2/pbkdf2.odin @@ -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 + } + } +} diff --git a/examples/all/all_main.odin b/examples/all/all_main.odin index fff344b22..cb7cd58a7 100644 --- a/examples/all/all_main.odin +++ b/examples/all/all_main.odin @@ -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 diff --git a/tests/core/crypto/test_core_crypto.odin b/tests/core/crypto/test_core_crypto.odin index 4ca34fc5a..df1076604 100644 --- a/tests/core/crypto/test_core_crypto.odin +++ b/tests/core/crypto/test_core_crypto.odin @@ -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) diff --git a/tests/core/crypto/test_core_crypto_kdf.odin b/tests/core/crypto/test_core_crypto_kdf.odin new file mode 100644 index 000000000..a1f2cbb9d --- /dev/null +++ b/tests/core/crypto/test_core_crypto_kdf.odin @@ -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, + ), + ) + } +}