implement scrypt kdf and it's tests suite (#41)

* implement scrypt kdf and it's tests suite

* fix typo and misc

* bump version to 0.5.1

* prevent OOM in appveyor 32 bit

* remove unnecessary exceptions

* fix missing pbkdf ctx

* scrypt mimicking pbkdf2 API

* compile time scrypt

* remove allocation and macros from scrypt

* fix copyright header in scrypt.nim
This commit is contained in:
andri lim 2020-08-02 23:20:42 +07:00 committed by GitHub
parent 8fdf6bde7a
commit 029a1f0f1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 421 additions and 2 deletions

View File

@ -1,6 +1,6 @@
# Package
version = "0.5.0"
version = "0.5.1"
author = "Eugene Kabanov"
description = "Nim cryptographic library"
license = "MIT"

260
nimcrypto/scrypt.nim Normal file
View File

@ -0,0 +1,260 @@
#
#
# NimCrypto
# (c) Copyright 2020 Andri Lim
#
# See the file "LICENSE", included in this
# distribution, for details about the copyright.
#
import utils, pbkdf2
## This module implements
## The scrypt Password-Based Key Derivation Function
## https://tools.ietf.org/html/rfc7914
proc salsaXor(tmp: var openArray[uint32],
src: openArray[uint32], srco: int, dest: var openArray[uint32], dsto: int) =
## salsaXor applies Salsa20/8 to the XOR of 16 numbers from tmp and in,
## and puts the result into both tmp and out.
# no macros policy
let
w0 = tmp[0] xor src[0 + srco]
w1 = tmp[1] xor src[1 + srco]
w2 = tmp[2] xor src[2 + srco]
w3 = tmp[3] xor src[3 + srco]
w4 = tmp[4] xor src[4 + srco]
w5 = tmp[5] xor src[5 + srco]
w6 = tmp[6] xor src[6 + srco]
w7 = tmp[7] xor src[7 + srco]
w8 = tmp[8] xor src[8 + srco]
w9 = tmp[9] xor src[9 + srco]
w10 = tmp[10] xor src[10 + srco]
w11 = tmp[11] xor src[11 + srco]
w12 = tmp[12] xor src[12 + srco]
w13 = tmp[13] xor src[13 + srco]
w14 = tmp[14] xor src[14 + srco]
w15 = tmp[15] xor src[15 + srco]
var
x0 = w0
x1 = w1
x2 = w2
x3 = w3
x4 = w4
x5 = w5
x6 = w6
x7 = w7
x8 = w8
x9 = w9
x10 = w10
x11 = w11
x12 = w12
x13 = w13
x14 = w14
x15 = w15
template R(x, a, b: untyped) =
x = x xor ROL(a, b)
for i in 0 ..< 4:
R(x4, x0+x12, 7); R(x8, x4+x0, 9)
R(x12, x8+x4, 13); R(x0, x12+x8, 18)
R(x9, x5+x1, 7); R(x13, x9+x5, 9)
R(x1, x13+x9, 13); R(x5, x1+x13, 18)
R(x14, x10+x6, 7); R(x2, x14+x10, 9)
R(x6, x2+x14, 13); R(x10, x6+x2, 18)
R(x3, x15+x11, 7); R(x7, x3+x15, 9)
R(x11, x7+x3, 13); R(x15, x11+x7, 18)
R(x1, x0+x3, 7); R(x2, x1+x0, 9)
R(x3, x2+x1, 13); R(x0, x3+x2, 18)
R(x6, x5+x4, 7); R(x7, x6+x5, 9)
R(x4, x7+x6, 13); R(x5, x4+x7, 18)
R(x11, x10+x9, 7); R(x8, x11+x10, 9)
R(x9, x8+x11, 13); R(x10, x9+x8, 18)
R(x12, x15+x14, 7); R(x13, x12+x15, 9)
R(x14, x13+x12, 13); R(x15, x14+x13, 18)
x0 += w0; x1 += w1; x2 += w2; x3 += w3
x4 += w4; x5 += w5; x6 += w6; x7 += w7
x8 += w8; x9 += w9; x10 += w10; x11 += w11
x12 += w12; x13 += w13; x14 += w14; x15 += w15
dest[0 + dsto] = x0; dest[1 + dsto] = x1
dest[2 + dsto] = x2; dest[3 + dsto] = x3
dest[4 + dsto] = x4; dest[5 + dsto] = x5
dest[6 + dsto] = x6; dest[7 + dsto] = x7
dest[8 + dsto] = x8; dest[9 + dsto] = x9
dest[10 + dsto] = x10; dest[11 + dsto] = x11
dest[12 + dsto] = x12; dest[13 + dsto] = x13
dest[14 + dsto] = x14; dest[15 + dsto] = x15
tmp[0] = x0; tmp[1] = x1; tmp[2] = x2
tmp[3] = x3; tmp[4] = x4; tmp[5] = x5
tmp[6] = x6; tmp[7] = x7; tmp[8] = x8
tmp[9] = x9; tmp[10] = x10; tmp[11] = x11
tmp[12] = x12; tmp[13] = x13; tmp[14] = x14
tmp[15] = x15
proc blockMix(tmp: var openArray[uint32], src: openArray[uint32], srco: int,
dest: var openArray[uint32], dsto: int, r: int) =
let
r16 = r*16
r2_1 = 2*r-1
var
i16 = srco
i8 = dsto
copyMem(tmp, 0, src, r2_1*16+srco, 16)
for i in countup(0, r2_1, 2):
salsaXor(tmp, src, i16, dest, i8)
salsaXor(tmp, src, i16+16, dest, i8+r16)
inc(i16, 32)
inc(i8, 16)
func integer(b: openArray[uint32], boff, r: int): uint64 =
let j = (2*r - 1) * 16 + boff
result = uint64(b[j]) or (uint64(b[j+1]) shl 32)
proc blockXor(dst: var openArray[uint32], dsto: int,
src: openArray[uint32], srco: int, n: int) =
## blockXor XORs numbers from dst with n numbers from src.
for i in 0 ..< n:
dst[i+dsto] = dst[i+dsto] xor src[i+srco]
proc smix(b: var openArray[byte], boffset, r, N: int,
xyv: var openArray[uint32], voffset: int) =
let
r32 = r * 32
N_1 = N - 1
template x: untyped = xyv
template v: untyped = xyv
template y: untyped = xyv
template yoffset: untyped = r32
var
# tmp: store xor'ed value for next step
tmp: array[16, uint32]
j = boffset
for i in 0 ..< r32:
x[i] = leLoad32(b, j)
inc(j, 4)
var n = voffset
for i in countup(0, N_1, 2):
# x[n] is an alias to v
copyMem(v, n, x, 0, r32)
blockMix(tmp, x, 0, y, yoffset, r)
inc(n, r32)
copyMem(v, n, y, yoffset, r32)
blockMix(tmp, y, yoffset, x, 0, r)
inc(n, r32)
for i in countup(0, N_1, 2):
j = int(integer(x, 0, r) and uint64(N_1))
blockXor(x, 0, v, j*r32+voffset, r32)
blockMix(tmp, x, 0, y, yoffset, r)
j = int(integer(y, yoffset, r) and uint64(N_1))
blockXor(y, yoffset, v, j*r32+voffset, r32)
blockMix(tmp, y, yoffset, x, 0, r)
j = boffset
for i in 0 ..< r32:
leStore32(b, j, x[i])
inc(j, 4)
func validateParam(N, r, p: int): bool =
# this function does not prevent DOS attack
# in case of using huge number
if N <= 1 or (N and (N-1)) != 0:
# N must be > 1 and a power of 2
return false
const
maxInt = high(int64)
maxIntd128 = maxInt div 128
maxIntd256 = maxInt div 256
let
badParam1 = uint64(r)*uint64(p) >= 1 shl 30
badParam2 = r > maxIntd128 div p
badParam3 = r > maxIntd256
badParam4 = N > maxIntd128 div r
if badParam1 or badParam2 or badParam3 or badParam4:
# parameters are too large
return false
result = true
# scrypt derives a key from the password, salt, and cost parameters, returning
# a byte slice of length keyLen that can be used as cryptographic key.
#
# N is a CPU/memory cost parameter, which must be a power of two greater than 1.
# r and p must satisfy r * p < 2^30. If the parameters do not satisfy the
# limits, the function return zero.
#
# Returns number of bytes stored on success, or 0 on error.
#
# For example, you can get a derived key for e.g. AES-256 (which needs a
# 32-byte key) by doing:
#
# dk = scrypt(some_password, salt, 32768, 8, 1, 32)
#
# The recommended parameters for interactive logins as of 2017 are N=32768, r=8
# and p=1. The parameters N, r, and p should be increased as memory latency and
# CPU parallelism increases; consider setting N to the highest power of 2 you
# can derive within 100 milliseconds. Remember to get a good random salt.
func scrypt*[T, M](password: openArray[T], salt: openArray[M],
N, r, p: int, xyv: var openArray[uint32],
b, output: var openArray[byte]): int =
when not((M is byte) or (M is char)):
{.fatal: "Choosen password type is not supported!".}
when not((T is byte) or (T is char)):
{.fatal: "Choosen salt type is not supported!".}
if not validateParam(N, r, p):
return 0
let
r32 = r*32
r64 = r32*2
r128 = r64*2
if b.len < p*r128:
return 0
if xyv.len < r32*(N+2):
return 0
var ctx: HMAC[sha256]
if ctx.pbkdf2(password, salt, 1, b) == 0:
return 0
var n = 0
for i in 0 ..< p:
smix(b, n, r, N, xyv, r64)
inc(n, r128)
ctx.pbkdf2(password, b, 1, output)
# because of NimCrypto no alloc policy,
# users of scrypt must provide their own `xyv` and `b` array.
# scryptCalc will return how much element needed.
# return value -> (xyv.len of uint32s, b.len of bytes)
func scryptCalc*(N, r, p: int): (int, int) =
result = ((r*32*(N+2)), (p*r*128))

View File

@ -1,6 +1,6 @@
import testsysrand
import testsha1, testsha2, testkeccak, testripemd, testblake2
import testhmac, testkdf
import testhmac, testkdf, testscrypt
import testrijndael, testblowfish, testtwofish
import testbcmode
import testapi, testutils

159
tests/testscrypt.nim Normal file
View File

@ -0,0 +1,159 @@
import unittest
# use include here, because we want to test
# internal functions too
include ../nimcrypto/scrypt
when defined(nimHasUsed): {.used.}
const
# these BE uint32 are copied from RFC 7914
# test vectors octets, and I'm too lazy to
# rewrite/reprint it into LE uint32
inputBE = [
0x7e879a21'u32, 0x4f3ec986'u32, 0x7ca940e6'u32, 0x41718f26'u32,
0xbaee555b'u32, 0x8c61c1b5'u32, 0x0df84611'u32, 0x6dcd3b1d'u32,
0xee24f319'u32, 0xdf9b3d85'u32, 0x14121e4b'u32, 0x5ac5aa32'u32,
0x76021d29'u32, 0x09c74829'u32, 0xedebc68d'u32, 0xb8b8c25e'u32
]
outputBE = [
0xa41f859c'u32, 0x6608cc99'u32, 0x3b81cacb'u32, 0x020cef05'u32,
0x044b2181'u32, 0xa2fd337d'u32, 0xfd7b1c63'u32, 0x96682f29'u32,
0xb4393168'u32, 0xe3c9e6bc'u32, 0xfe6bc5b7'u32, 0xa06d96ba'u32,
0xe424cc10'u32, 0x2c91745c'u32, 0x24ad673d'u32, 0xc7618f81'u32
]
bInBE = [
0xf7ce0b65'u32, 0x3d2d72a4'u32, 0x108cf5ab'u32, 0xe912ffdd'u32,
0x777616db'u32, 0xbb27a70e'u32, 0x8204f3ae'u32, 0x2d0f6fad'u32,
0x89f68f48'u32, 0x11d1e87b'u32, 0xcc3bd740'u32, 0x0a9ffd29'u32,
0x094f0184'u32, 0x639574f3'u32, 0x9ae5a131'u32, 0x5217bcd7'u32,
0x89499144'u32, 0x7213bb22'u32, 0x6c25b54d'u32, 0xa86370fb'u32,
0xcd984380'u32, 0x374666bb'u32, 0x8ffcb5bf'u32, 0x40c254b0'u32,
0x67d27c51'u32, 0xce4ad5fe'u32, 0xd829c90b'u32, 0x505a571b'u32,
0x7f4d1cad'u32, 0x6a523cda'u32, 0x770e67bc'u32, 0xeaaf7e89'u32
]
bOutBE = [
0xa41f859c'u32, 0x6608cc99'u32, 0x3b81cacb'u32, 0x020cef05'u32,
0x044b2181'u32, 0xa2fd337d'u32, 0xfd7b1c63'u32, 0x96682f29'u32,
0xb4393168'u32, 0xe3c9e6bc'u32, 0xfe6bc5b7'u32, 0xa06d96ba'u32,
0xe424cc10'u32, 0x2c91745c'u32, 0x24ad673d'u32, 0xc7618f81'u32,
0x20edc975'u32, 0x323881a8'u32, 0x0540f64c'u32, 0x162dcd3c'u32,
0x21077cfe'u32, 0x5f8d5fe2'u32, 0xb1a4168f'u32, 0x953678b7'u32,
0x7d3b3d80'u32, 0x3b60e4ab'u32, 0x920996e5'u32, 0x9b4d53b6'u32,
0x5d2a2258'u32, 0x77d5edf5'u32, 0x842cb9f1'u32, 0x4eefe425'u32
]
bhex =
"f7ce0b653d2d72a4108cf5abe912ffdd" &
"777616dbbb27a70e8204f3ae2d0f6fad" &
"89f68f4811d1e87bcc3bd7400a9ffd29" &
"094f0184639574f39ae5a1315217bcd7" &
"894991447213bb226c25b54da86370fb" &
"cd984380374666bb8ffcb5bf40c254b0" &
"67d27c51ce4ad5fed829c90b505a571b" &
"7f4d1cad6a523cda770e67bceaaf7e89"
bouthex =
"79ccc193629debca047f0b70604bf6b6" &
"2ce3dd4a9626e355fafc6198e6ea2b46" &
"d58413673b99b029d665c357601fb426" &
"a0b2f4bba200ee9f0a43d19b571a9c71" &
"ef1142e65d5a266fddca832ce59faa7c" &
"ac0b9cf1be2bffca300d01ee387619c4" &
"ae12fd4438f203a0e4e1c47ec314861f" &
"4e9087cb33396a6873e8f9d2539a4b8e"
proc swapBytes(x: var openArray[uint32]) =
for i in 0..<x.len:
x[i] = leSwap32(x[i])
proc swapBytes(a: var openArray[uint32], b: openArray[uint32]) =
for i in 0..<a.len:
a[i] = leSwap32(b[i])
suite "Scrypt KDF tests suite":
test "salsaXor":
var input: array[16, uint32]
var output: array[16, uint32]
var tmp: array[16, uint32]
input.swapBytes(inputBE)
salsaXor(tmp, input, 0, output, 0)
output.swapBytes
check output == outputBE
test "blockMix":
var bIn: array[32, uint32]
var bOut: array[32, uint32]
var bTmp: array[32, uint32]
bIn.swapBytes(bInBE)
blockMix(bTmp, bIn, 0, bOut, 0, 1)
bOut.swapBytes
check bOut == bOutBE
test "smix":
let
r = 1
N = 16
var xy = newSeq[uint32](64*r + 32*N*r)
var b = utils.fromHex(bhex)
smix(b, 0, r, N, xy, 64*r)
var bb = utils.fromHex(bouthex)
check b == bb
func scrypt[T, M](password: openArray[T], salt: openArray[M],
N, r, p, keyLen: static[int]): array[keyLen, byte] =
let (xyvLen, bLen) = scryptCalc(N, r, p)
var xyv = newSeq[uint32](xyvLen)
var b = newSeq[byte](bLen)
discard scrypt(password, salt, N, r, p, xyv, b, result)
# again, these test vectors are copied from RFC 7914
test "scrypt N=16, r=1, p=1, keyLen=64":
let key = fromHex("77D6576238657B203B19CA42C18A0497F16B4844E3074AE8DFDFFA3FEDE21442" &
"FCD0069DED0948F8326A753A0FC81F17E8D3E0FB2E0D3628CF35E20C38D18906")
let dkey = scrypt(password="", salt="", N=16, r=1, p=1, keyLen=64)
check key == dkey
test "scrypt N=16, r=1, p=1, keyLen=64 (compile-time)":
const key = fromHex("77D6576238657B203B19CA42C18A0497F16B4844E3074AE8DFDFFA3FEDE21442" &
"FCD0069DED0948F8326A753A0FC81F17E8D3E0FB2E0D3628CF35E20C38D18906")
const dkey = scrypt(password="", salt="", N=16, r=1, p=1, keyLen=64)
check key == dkey
test "scrypt N=1024, r=8, p=16, keyLen=64":
let key = fromHex("FDBABE1C9D3472007856E7190D01E9FE7C6AD7CBC8237830E77376634B373162" &
"2EAF30D92E22A3886FF109279D9830DAC727AFB94A83EE6D8360CBDFA2CC0640")
let dkey = scrypt(password="password", salt="NaCl", N=1024, r=8, p=16, keyLen=64)
check key == dkey
test "scrypt N=16384, r=8, p=1, keyLen=64":
let key = fromHex("7023BDCB3AFD7348461C06CD81FD38EBFDA8FBBA904F8E3EA9B543F6545DA1F2" &
"D5432955613F0FCF62D49705242A9AF9E61E85DC0D651E40DFCF017B45575887")
let dkey = scrypt(password="pleaseletmein", salt="SodiumChloride", N=16384, r=8, p=1, keyLen=64)
check key == dkey
when defined(cpu64):
# these test vectors OOM with appveyor 32 bit
# because of huge N
test "scrypt N=1048576, r=8, p=1, keyLen=32":
let key = fromHex("E277EA2CACB23EDAFC039D229B79DC13ECEDB601D99B182A9FEDBA1E2BFB4F58")
let dkey = scrypt(password="Rabbit", salt="Mouse", N=1048576, r=8, p=1, keyLen=32)
check key == dkey
test "scrypt N=1048576, r=8, p=1, keyLen=64":
let key = fromHex("2101CB9B6A511AAEADDBBE09CF70F881EC568D574A2FFD4DABE5EE9820ADAA47" &
"8E56FD8F4BA5D09FFA1C6D927C40F4C337304049E8A952FBCBF45C6FA77A41A4")
let dkey = scrypt(password="pleaseletmein", salt="SodiumChloride", N=1048576, r=8, p=1, keyLen=64)
check key == dkey
test "string vs openarray[byte]":
let stringarg = scrypt(password="password", salt="NaCl", N=1024, r=8, p=16, keyLen=32)
let openarrayarg = scrypt(fromHex("70617373776F7264"), fromHex("4E61436C"), N=1024, r=8, p=16, keyLen=32)
check stringarg == openarrayarg