Browse Source

core/crypto/pbkdf2: Initial import

Yawning Angel 1 year ago
parent
commit
290168f862

+ 122 - 0
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
+		}
+	}
+}

+ 2 - 0
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

+ 1 - 0
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)

+ 119 - 0
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,
+			),
+		)
+	}
+}