From 423f804b0756a84a2daa0341e68695c02466db20 Mon Sep 17 00:00:00 2001 From: Kim Shrier Date: Sat, 14 Sep 2024 00:54:04 -0600 Subject: [PATCH] crypto.scrypt: add a new `scrypt` module to vlib/crypto (#22216) --- vlib/crypto/scrypt/scrypt.v | 241 +++++++++++++++++++++++++++++++ vlib/crypto/scrypt/scrypt_test.v | 229 +++++++++++++++++++++++++++++ 2 files changed, 470 insertions(+) create mode 100644 vlib/crypto/scrypt/scrypt.v create mode 100644 vlib/crypto/scrypt/scrypt_test.v diff --git a/vlib/crypto/scrypt/scrypt.v b/vlib/crypto/scrypt/scrypt.v new file mode 100644 index 0000000000..a843394d88 --- /dev/null +++ b/vlib/crypto/scrypt/scrypt.v @@ -0,0 +1,241 @@ +// Copyright (c) 2023 Kim Shrier. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +// Package scrypt implements the key derivation functions as +// described in https://datatracker.ietf.org/doc/html/rfc7914 +module scrypt + +import crypto.pbkdf2 +import crypto.sha256 +import encoding.binary +import math.bits + +pub const max_buffer_length = ((u64(1) << 32) - 1) * 32 +pub const max_blocksize_parallal_product = u64(1 << 30) + +// salsa20_8 applies the salsa20/8 core transformation to a block +// of 64 u8 bytes. The block is modified in place. +fn salsa20_8(mut block []u8) { + mut block_words := []u32{len: 16} + mut scratch := [16]u32{} + + for i in 0 .. 16 { + block_words[i] = binary.little_endian_u32_at(block, i * 4) + scratch[i] = block_words[i] + } + + for i := 8; i > 0; i -= 2 { + // processing columns + scratch[4] ^= bits.rotate_left_32(scratch[0] + scratch[12], 7) + scratch[8] ^= bits.rotate_left_32(scratch[4] + scratch[0], 9) + scratch[12] ^= bits.rotate_left_32(scratch[8] + scratch[4], 13) + scratch[0] ^= bits.rotate_left_32(scratch[12] + scratch[8], 18) + + scratch[9] ^= bits.rotate_left_32(scratch[5] + scratch[1], 7) + scratch[13] ^= bits.rotate_left_32(scratch[9] + scratch[5], 9) + scratch[1] ^= bits.rotate_left_32(scratch[13] + scratch[9], 13) + scratch[5] ^= bits.rotate_left_32(scratch[1] + scratch[13], 18) + + scratch[14] ^= bits.rotate_left_32(scratch[10] + scratch[6], 7) + scratch[2] ^= bits.rotate_left_32(scratch[14] + scratch[10], 9) + scratch[6] ^= bits.rotate_left_32(scratch[2] + scratch[14], 13) + scratch[10] ^= bits.rotate_left_32(scratch[6] + scratch[2], 18) + + scratch[3] ^= bits.rotate_left_32(scratch[15] + scratch[11], 7) + scratch[7] ^= bits.rotate_left_32(scratch[3] + scratch[15], 9) + scratch[11] ^= bits.rotate_left_32(scratch[7] + scratch[3], 13) + scratch[15] ^= bits.rotate_left_32(scratch[11] + scratch[7], 18) + + // processing rows + scratch[1] ^= bits.rotate_left_32(scratch[0] + scratch[3], 7) + scratch[2] ^= bits.rotate_left_32(scratch[1] + scratch[0], 9) + scratch[3] ^= bits.rotate_left_32(scratch[2] + scratch[1], 13) + scratch[0] ^= bits.rotate_left_32(scratch[3] + scratch[2], 18) + + scratch[6] ^= bits.rotate_left_32(scratch[5] + scratch[4], 7) + scratch[7] ^= bits.rotate_left_32(scratch[6] + scratch[5], 9) + scratch[4] ^= bits.rotate_left_32(scratch[7] + scratch[6], 13) + scratch[5] ^= bits.rotate_left_32(scratch[4] + scratch[7], 18) + + scratch[11] ^= bits.rotate_left_32(scratch[10] + scratch[9], 7) + scratch[8] ^= bits.rotate_left_32(scratch[11] + scratch[10], 9) + scratch[9] ^= bits.rotate_left_32(scratch[8] + scratch[11], 13) + scratch[10] ^= bits.rotate_left_32(scratch[9] + scratch[8], 18) + + scratch[12] ^= bits.rotate_left_32(scratch[15] + scratch[14], 7) + scratch[13] ^= bits.rotate_left_32(scratch[12] + scratch[15], 9) + scratch[14] ^= bits.rotate_left_32(scratch[13] + scratch[12], 13) + scratch[15] ^= bits.rotate_left_32(scratch[14] + scratch[13], 18) + } + + for i in 0 .. 16 { + scratch[i] += block_words[i] + binary.little_endian_put_u32_at(mut block, scratch[i], i * 4) + } +} + +@[inline] +fn blkcpy(mut dest []u8, src []u8, len u32) { + for i in 0 .. len { + dest[i] = src[i] + } +} + +@[inline] +fn blkxor(mut dest []u8, src []u8, len u32) { + for i in 0 .. len { + dest[i] ^= src[i] + } +} + +// block_mix performs the block_mix operation using salsa20_8 +// +// The block input must be 128 * r in length. The temp array +// has to be the same size, 128 * r. r is a positive integer +// value > 0. The block is modified in place. +fn block_mix(mut block []u8, mut temp []u8, r u32) { + mut scratch := []u8{len: 64, cap: 64} + + blkcpy(mut scratch, block[(((2 * r) - 1) * 64)..], 64) + + for i in 0 .. 2 * r { + start := i * 64 + stop := start + 64 + + blkxor(mut scratch, block[start..stop], 64) + salsa20_8(mut scratch) + + blkcpy(mut temp[start..stop], scratch, 64) + } + + for i in 0 .. r { + start := i * 64 + stop := start + 64 + + temp_start := (i * 2) * 64 + temp_stop := temp_start + 64 + + blkcpy(mut block[start..stop], temp[temp_start..temp_stop], 64) + } + + for i in 0 .. r { + start := (i + r) * 64 + stop := start + 64 + + temp_start := ((i * 2) + 1) * 64 + temp_stop := temp_start + 64 + + blkcpy(mut block[start..stop], temp[temp_start..temp_stop], 64) + } +} + +fn smix(mut block []u8, r u32, n u64, mut v_block []u8, mut temp_block []u8) { + blkcpy(mut temp_block, block, 128 * r) + + y_start := 128 * r + + for i in 0 .. n { + v_start := i * (128 * r) + v_stop := v_start + (128 * r) + + blkcpy(mut v_block[v_start..v_stop], temp_block, 128 * r) + block_mix(mut temp_block, mut temp_block[y_start..], r) + } + + for _ in 0 .. n { + j := binary.little_endian_u64_at(temp_block, ((2 * r) - 1) * 64) & (n - 1) + + v_start := j * (128 * r) + v_stop := v_start + (128 * r) + + blkxor(mut temp_block, v_block[v_start..v_stop], 128 * r) + block_mix(mut temp_block, mut temp_block[y_start..], r) + } + + blkcpy(mut block, temp_block, 128 * r) +} + +struct OutputBufferLengthError { + Error + length u64 +} + +fn (err OutputBufferLengthError) msg() string { + return 'the output buffer length, ${err.length}, is greater than ${max_buffer_length}' +} + +struct BlocksizeParallelProductError { + Error + blocksize u32 + parallel u32 + product u64 +} + +fn (err BlocksizeParallelProductError) msg() string { + return 'the product of blocksize ${err.blocksize} * parallel ${err.parallel} = ${err.product}, is greater than ${max_blocksize_parallal_product}' +} + +struct CpuMemoryCostError { + Error + cost u64 +} + +fn (err CpuMemoryCostError) msg() string { + return 'the CPU/memory cost ${err.cost} must be greater than 0 and also a power of 2' +} + +// scrypt performs password based key derivation using the scrypt algorithm. +// +// The input parameters are: +// +// password - a slice of bytes which is the password being used to +// derive the key. Don't leak this value to anybody. +// salt - a slice of bytes used to make it harder to crack the key. +// n - CPU/Memory cost parameter, must be larger than 0, a power of 2, +// and less than 2^(128 * r / 8). +// r - block size parameter. +// p - parallelization parameter, a positive integer less than or +// equal to ((2^32-1) * hLen) / MFLen where hLen is 32 and +// MFlen is 128 * r. +// dk_len - intended output length in octets of the derived key; +// a positive integer less than or equal to (2^32 - 1) * hLen +// where hLen is 32. +// +// Reasonable values for n, r, and p are n = 1024, r = 8, p = 16. +pub fn scrypt(password []u8, salt []u8, n u64, r u32, p u32, dk_len u64) ![]u8 { + if dk_len > max_buffer_length { + return OutputBufferLengthError{ + length: dk_len + } + } + + if u64(r) * u64(p) >= max_blocksize_parallal_product { + return BlocksizeParallelProductError{ + blocksize: r + parallel: p + product: u64(r) * u64(p) + } + } + + // the following is a sneaky way to determine if a number is a + // power of 2. Also, a value of 0 is not allowed. + if (n & (n - 1)) != 0 || n == 0 { + return CpuMemoryCostError{ + cost: n + } + } + + mut b := pbkdf2.key(password, salt, 1, 128 * r * p, sha256.new())! + + mut xy := []u8{len: int(256 * r), cap: int(256 * r), init: 0} + mut v := []u8{len: int(128 * r * n), cap: int(128 * r * n), init: 0} + + for i in u32(0) .. p { + smix(mut b[i * 128 * r..], r, n, mut v, mut xy) + } + + result := pbkdf2.key(password, b, 1, 128 * r * p, sha256.new())! + + return result[..dk_len] +} diff --git a/vlib/crypto/scrypt/scrypt_test.v b/vlib/crypto/scrypt/scrypt_test.v new file mode 100644 index 0000000000..a1020b5da6 --- /dev/null +++ b/vlib/crypto/scrypt/scrypt_test.v @@ -0,0 +1,229 @@ +module scrypt + +import crypto.pbkdf2 +import crypto.sha256 + +fn test_salsa20_8() { + // The input_block and output_block values are taken from + // [RFC7914](https://datatracker.ietf.org/doc/html/rfc7914#section-8) + // section 8. + + // vfmt off + mut input_block := [ + u8(0x7e), 0x87, 0x9a, 0x21, 0x4f, 0x3e, 0xc9, 0x86, + 0x7c, 0xa9, 0x40, 0xe6, 0x41, 0x71, 0x8f, 0x26, + 0xba, 0xee, 0x55, 0x5b, 0x8c, 0x61, 0xc1, 0xb5, + 0x0d, 0xf8, 0x46, 0x11, 0x6d, 0xcd, 0x3b, 0x1d, + 0xee, 0x24, 0xf3, 0x19, 0xdf, 0x9b, 0x3d, 0x85, + 0x14, 0x12, 0x1e, 0x4b, 0x5a, 0xc5, 0xaa, 0x32, + 0x76, 0x02, 0x1d, 0x29, 0x09, 0xc7, 0x48, 0x29, + 0xed, 0xeb, 0xc6, 0x8d, 0xb8, 0xb8, 0xc2, 0x5e] + + output_block := [ + u8(0xa4), 0x1f, 0x85, 0x9c, 0x66, 0x08, 0xcc, 0x99, + 0x3b, 0x81, 0xca, 0xcb, 0x02, 0x0c, 0xef, 0x05, + 0x04, 0x4b, 0x21, 0x81, 0xa2, 0xfd, 0x33, 0x7d, + 0xfd, 0x7b, 0x1c, 0x63, 0x96, 0x68, 0x2f, 0x29, + 0xb4, 0x39, 0x31, 0x68, 0xe3, 0xc9, 0xe6, 0xbc, + 0xfe, 0x6b, 0xc5, 0xb7, 0xa0, 0x6d, 0x96, 0xba, + 0xe4, 0x24, 0xcc, 0x10, 0x2c, 0x91, 0x74, 0x5c, + 0x24, 0xad, 0x67, 0x3d, 0xc7, 0x61, 0x8f, 0x81] + // vfmt on + + salsa20_8(mut input_block) + + for i in 0 .. 64 { + assert input_block[i] == output_block[i], 'assertion failed for i: ${i}' + } +} + +fn test_block_mix() { + // The input_block and output_block values are taken from + // [RFC7914](https://datatracker.ietf.org/doc/html/rfc7914#section-9) + // section 9. + + // vfmt off + mut input_block := [ + // B[0] - the first 64 bytes of input + u8(0xf7), 0xce, 0x0b, 0x65, 0x3d, 0x2d, 0x72, 0xa4, + 0x10, 0x8c, 0xf5, 0xab, 0xe9, 0x12, 0xff, 0xdd, + 0x77, 0x76, 0x16, 0xdb, 0xbb, 0x27, 0xa7, 0x0e, + 0x82, 0x04, 0xf3, 0xae, 0x2d, 0x0f, 0x6f, 0xad, + 0x89, 0xf6, 0x8f, 0x48, 0x11, 0xd1, 0xe8, 0x7b, + 0xcc, 0x3b, 0xd7, 0x40, 0x0a, 0x9f, 0xfd, 0x29, + 0x09, 0x4f, 0x01, 0x84, 0x63, 0x95, 0x74, 0xf3, + 0x9a, 0xe5, 0xa1, 0x31, 0x52, 0x17, 0xbc, 0xd7, + // B[1] - the second 64 bytes of input + 0x89, 0x49, 0x91, 0x44, 0x72, 0x13, 0xbb, 0x22, + 0x6c, 0x25, 0xb5, 0x4d, 0xa8, 0x63, 0x70, 0xfb, + 0xcd, 0x98, 0x43, 0x80, 0x37, 0x46, 0x66, 0xbb, + 0x8f, 0xfc, 0xb5, 0xbf, 0x40, 0xc2, 0x54, 0xb0, + 0x67, 0xd2, 0x7c, 0x51, 0xce, 0x4a, 0xd5, 0xfe, + 0xd8, 0x29, 0xc9, 0x0b, 0x50, 0x5a, 0x57, 0x1b, + 0x7f, 0x4d, 0x1c, 0xad, 0x6a, 0x52, 0x3c, 0xda, + 0x77, 0x0e, 0x67, 0xbc, 0xea, 0xaf, 0x7e, 0x89] + + output_block := [ + // B'[0] - the first 64 bytes of output + u8(0xa4), 0x1f, 0x85, 0x9c, 0x66, 0x08, 0xcc, 0x99, + 0x3b, 0x81, 0xca, 0xcb, 0x02, 0x0c, 0xef, 0x05, + 0x04, 0x4b, 0x21, 0x81, 0xa2, 0xfd, 0x33, 0x7d, + 0xfd, 0x7b, 0x1c, 0x63, 0x96, 0x68, 0x2f, 0x29, + 0xb4, 0x39, 0x31, 0x68, 0xe3, 0xc9, 0xe6, 0xbc, + 0xfe, 0x6b, 0xc5, 0xb7, 0xa0, 0x6d, 0x96, 0xba, + 0xe4, 0x24, 0xcc, 0x10, 0x2c, 0x91, 0x74, 0x5c, + 0x24, 0xad, 0x67, 0x3d, 0xc7, 0x61, 0x8f, 0x81, + // B'[1] - the second 64 bytes of output + 0x20, 0xed, 0xc9, 0x75, 0x32, 0x38, 0x81, 0xa8, + 0x05, 0x40, 0xf6, 0x4c, 0x16, 0x2d, 0xcd, 0x3c, + 0x21, 0x07, 0x7c, 0xfe, 0x5f, 0x8d, 0x5f, 0xe2, + 0xb1, 0xa4, 0x16, 0x8f, 0x95, 0x36, 0x78, 0xb7, + 0x7d, 0x3b, 0x3d, 0x80, 0x3b, 0x60, 0xe4, 0xab, + 0x92, 0x09, 0x96, 0xe5, 0x9b, 0x4d, 0x53, 0xb6, + 0x5d, 0x2a, 0x22, 0x58, 0x77, 0xd5, 0xed, 0xf5, + 0x84, 0x2c, 0xb9, 0xf1, 0x4e, 0xef, 0xe4, 0x25] + // vfmt on + + // an array capable of holding r * 128 bytes used during + // the block_mix operation. + mut temp_block := []u8{len: 128, cap: 128} + + // for this test, r = 1 + block_mix(mut input_block, mut temp_block, 1) + + for i in 0 .. 128 { + assert input_block[i] == output_block[i], 'assertion failed for i: ${i}' + } +} + +fn test_smix() { + // The input_block and output_block values are taken from + // [RFC7914](https://datatracker.ietf.org/doc/html/rfc7914#section-10) + // section 10. + + // vfmt off + mut input_block := [ + u8(0xf7), 0xce, 0x0b, 0x65, 0x3d, 0x2d, 0x72, 0xa4, + 0x10, 0x8c, 0xf5, 0xab, 0xe9, 0x12, 0xff, 0xdd, + 0x77, 0x76, 0x16, 0xdb, 0xbb, 0x27, 0xa7, 0x0e, + 0x82, 0x04, 0xf3, 0xae, 0x2d, 0x0f, 0x6f, 0xad, + 0x89, 0xf6, 0x8f, 0x48, 0x11, 0xd1, 0xe8, 0x7b, + 0xcc, 0x3b, 0xd7, 0x40, 0x0a, 0x9f, 0xfd, 0x29, + 0x09, 0x4f, 0x01, 0x84, 0x63, 0x95, 0x74, 0xf3, + 0x9a, 0xe5, 0xa1, 0x31, 0x52, 0x17, 0xbc, 0xd7, + 0x89, 0x49, 0x91, 0x44, 0x72, 0x13, 0xbb, 0x22, + 0x6c, 0x25, 0xb5, 0x4d, 0xa8, 0x63, 0x70, 0xfb, + 0xcd, 0x98, 0x43, 0x80, 0x37, 0x46, 0x66, 0xbb, + 0x8f, 0xfc, 0xb5, 0xbf, 0x40, 0xc2, 0x54, 0xb0, + 0x67, 0xd2, 0x7c, 0x51, 0xce, 0x4a, 0xd5, 0xfe, + 0xd8, 0x29, 0xc9, 0x0b, 0x50, 0x5a, 0x57, 0x1b, + 0x7f, 0x4d, 0x1c, 0xad, 0x6a, 0x52, 0x3c, 0xda, + 0x77, 0x0e, 0x67, 0xbc, 0xea, 0xaf, 0x7e, 0x89] + + output_block := [ + u8(0x79), 0xcc, 0xc1, 0x93, 0x62, 0x9d, 0xeb, 0xca, + 0x04, 0x7f, 0x0b, 0x70, 0x60, 0x4b, 0xf6, 0xb6, + 0x2c, 0xe3, 0xdd, 0x4a, 0x96, 0x26, 0xe3, 0x55, + 0xfa, 0xfc, 0x61, 0x98, 0xe6, 0xea, 0x2b, 0x46, + 0xd5, 0x84, 0x13, 0x67, 0x3b, 0x99, 0xb0, 0x29, + 0xd6, 0x65, 0xc3, 0x57, 0x60, 0x1f, 0xb4, 0x26, + 0xa0, 0xb2, 0xf4, 0xbb, 0xa2, 0x00, 0xee, 0x9f, + 0x0a, 0x43, 0xd1, 0x9b, 0x57, 0x1a, 0x9c, 0x71, + 0xef, 0x11, 0x42, 0xe6, 0x5d, 0x5a, 0x26, 0x6f, + 0xdd, 0xca, 0x83, 0x2c, 0xe5, 0x9f, 0xaa, 0x7c, + 0xac, 0x0b, 0x9c, 0xf1, 0xbe, 0x2b, 0xff, 0xca, + 0x30, 0x0d, 0x01, 0xee, 0x38, 0x76, 0x19, 0xc4, + 0xae, 0x12, 0xfd, 0x44, 0x38, 0xf2, 0x03, 0xa0, + 0xe4, 0xe1, 0xc4, 0x7e, 0xc3, 0x14, 0x86, 0x1f, + 0x4e, 0x90, 0x87, 0xcb, 0x33, 0x39, 0x6a, 0x68, + 0x73, 0xe8, 0xf9, 0xd2, 0x53, 0x9a, 0x4b, 0x8e] + // vfmt on + + r := u32(1) + n := u64(16) + + // len and cap are 128 * r * n = 2048 + mut v_block := []u8{len: 2048, cap: 2048} + + // len and cap are 256 * r = 246 + mut temp_block := []u8{len: 256, cap: 256} + + smix(mut input_block, r, n, mut v_block, mut temp_block) + + for i in 0 .. 128 { + assert input_block[i] == output_block[i], 'assertion failed for i: ${i}' + } +} + +fn test_pbkdf2_hmac_sha256() { + // The input_block and output_block values are taken from + // [RFC7914](https://datatracker.ietf.org/doc/html/rfc7914#section-11) + // section 11. + + // vfmt off + output_block := [ + [u8(0x55), 0xac, 0x04, 0x6e, 0x56, 0xe3, 0x08, 0x9f, + 0xec, 0x16, 0x91, 0xc2, 0x25, 0x44, 0xb6, 0x05, + 0xf9, 0x41, 0x85, 0x21, 0x6d, 0xde, 0x04, 0x65, + 0xe6, 0x8b, 0x9d, 0x57, 0xc2, 0x0d, 0xac, 0xbc, + 0x49, 0xca, 0x9c, 0xcc, 0xf1, 0x79, 0xb6, 0x45, + 0x99, 0x16, 0x64, 0xb3, 0x9d, 0x77, 0xef, 0x31, + 0x7c, 0x71, 0xb8, 0x45, 0xb1, 0xe3, 0x0b, 0xd5, + 0x09, 0x11, 0x20, 0x41, 0xd3, 0xa1, 0x97, 0x83 + ], + [u8(0x4d), 0xdc, 0xd8, 0xf6, 0x0b, 0x98, 0xbe, 0x21, + 0x83, 0x0c, 0xee, 0x5e, 0xf2, 0x27, 0x01, 0xf9, + 0x64, 0x1a, 0x44, 0x18, 0xd0, 0x4c, 0x04, 0x14, + 0xae, 0xff, 0x08, 0x87, 0x6b, 0x34, 0xab, 0x56, + 0xa1, 0xd4, 0x25, 0xa1, 0x22, 0x58, 0x33, 0x54, + 0x9a, 0xdb, 0x84, 0x1b, 0x51, 0xc9, 0xb3, 0x17, + 0x6a, 0x27, 0x2b, 0xde, 0xbb, 0xa1, 0xd0, 0x78, + 0x47, 0x8f, 0x62, 0xb3, 0x97, 0xf3, 0x3c, 0x8d + ] + ] + // vfmt on + + d0 := pbkdf2.key('passwd'.bytes(), 'salt'.bytes(), 1, 64, sha256.new())! + + assert d0 == output_block[0] + + d1 := pbkdf2.key('Password'.bytes(), 'NaCl'.bytes(), 80000, 64, sha256.new())! + + assert d1 == output_block[1] +} + +struct ScryptTestData { + name string + password []u8 + salt []u8 + n u64 + r u32 + p u32 + dk_len u64 + expected_result []u8 +} + +const scrypt_test_cases = [ + ScryptTestData{ + name: 'test case 1' + password: ''.bytes() + salt: ''.bytes() + n: 16 + r: 1 + p: 1 + dk_len: 64 + expected_result: [u8(0x77), 0xd6, 0x57, 0x62, 0x38, 0x65, 0x7b, 0x20, 0x3b, 0x19, 0xca, + 0x42, 0xc1, 0x8a, 0x04, 0x97, 0xf1, 0x6b, 0x48, 0x44, 0xe3, 0x07, 0x4a, 0xe8, 0xdf, + 0xdf, 0xfa, 0x3f, 0xed, 0xe2, 0x14, 0x42, 0xfc, 0xd0, 0x06, 0x9d, 0xed, 0x09, 0x48, + 0xf8, 0x32, 0x6a, 0x75, 0x3a, 0x0f, 0xc8, 0x1f, 0x17, 0xe8, 0xd3, 0xe0, 0xfb, 0x2e, + 0x0d, 0x36, 0x28, 0xcf, 0x35, 0xe2, 0x0c, 0x38, 0xd1, 0x89, 0x06] + }, + // test cases 2, 3, and 4 are moved to the slow test repo. +] + +fn test_scrypt() { + for c in scrypt_test_cases { + results := scrypt(c.password, c.salt, c.n, c.r, c.p, c.dk_len)! + assert results == c.expected_result, '${c.name} failed' + } +}