scrypt基于密码的密钥派生函数(译)

原文地址:http://rossihwang.farbox.com/post/2014-03-25

原文请参考
The scrypt Password-Based Key Derivation Function
[连接]


1.导言

在密码学中,基于密码的“密钥派生函数”(key derivation functions)被用于从一个密值(secret value)中派生出一个或多个秘钥。多年来,多种基于密码的密钥派生函数已被使用,其中包括最初的DEs-based UNIX Crypt-function,FreeBSD MD5 crpty,PKCS#5 PBKDF2(一般使用SHA-1),GNU SHA-256/512 crypt,Windows NT LAN Manager(NTLM) hash,还有Blowfish-based bcrypt。这些算法都是基于相似的技术,他们都利用了加密学原语,加盐与/或迭代。迭代的次数用于减慢计算。

伴随着计算机系统的加速,迭代的数量也需要增加。合法用户只需花一定的时间去派生密钥,而不必像攻击者那样需要一直提高算力,不过前提是攻击者与合法用户使用同样的软件。然而,正如Bernstein指出,在整数分解的情况下,相对于软件实现并行硬件实现可能不能改变运算的次数。(省略一大段)。美元-秒模型是衡量计算损耗的最合适方法。随着半导体技术发展,电路并不只是变得更快;他们也变得更小,使得在同样资源下,允许更多的并行。总的来说,目前的密钥派生算法,不管迭代次数怎么增加,校验一个密码的时间是一定的,而通过硬件暴力破解去获取密码所花费的(能量或时间)逐年减少。

Scrypt算法目的在于减弱攻击者可以通过并行电路获得的优势,这种定制的并行电路专用于破解密钥派生函数。

余下文章分为几个部分,每一部分都描述一种算法。而这通过几种算法将以递进的方式推出scrypt算法。


2.Salsa20/8 Core函数

Salsa20/8 Core是Salsa20的一种round-reduced变体。他是一个哈希函数,输入64字节字符串,输出64字节字符串。值得注意的是,Salsa20/8 Core不是一种密码学哈希函数,因为他没有抗冲突性(collision-resistant)。

Salsa20 Core的定义

Salsa20的64字节输入从小端模式看就是16个字(word,四个字节)x0,x1,x2,...,x15。这16个字要经过320次可逆的变换,在每次变换中只改变一个字。最终16个字被分别加到原始的x0,x1,x2,...,x15上并对2^32取模,Salsa20(x)以小端模式输出。

每次变换就是将两个字的和的循环移位值(下面的R(a, b))异或到一个字上(第三个字),并对2^32取模。所以这320次变换包含一共320次加法运算,320次异或运算和320次旋转运算。其中旋转运算都是常数距离。

整个转换的过程是一个十次相同的“双循环”序列。每一个“双循环“都是两个循环的序列。每一个循环又包含一组可并行的”四分一循环“。每个”四分一循环“中变换四个字。
完整函数定义如下:

#define R(a, b) (((a) << (b) | (a) >> (32 - (b))))  //交换a的低(b - 32)为与a的高b位交换
void salsa20_word_specification(uint32 out[16], uint32 in[16]) {
    int i;
    uint32 x[16];
    for(i = 0; i < 16; ++i) {
        x[i] = in[i];
    }
    for(i = 20; i > 0; i -= 2) {  //注:ltc里用的是salsa8,所以这里20应改为8

        x[4] ^= R(x[0]+x[12], 7);   
        x[8] ^= R(x[4]+x[0], 9);
        x[12] ^= R(x[8]+x[4], 13);   
        x[0] ^= R(x[12]+x[ 8], 18);

        x[9] ^= R(x[5]+x[1], 7);   
        x[13] ^= R(x[9]+x[5], 9);
        x[1] ^= R(x[13]+x[9], 13);   
        x[5] ^= R(x[1]+x[13], 18);

        x[14] ^= R(x[10]+x[6], 7);   
        x[2] ^= R(x[14]+x[10], 9);
        x[6] ^= R(x[2]+x[14], 13);   
        x[10] ^= R(x[6]+x[2], 18);

        x[3] ^= R(x[15]+x[11], 7);   
        x[7] ^= R(x[3]+x[15], 9);
        x[11] ^= R(x[7]+x[3], 13);   
        x[15] ^= R(x[11]+x[ 7],18);

        x[ 1] ^= R(x[ 0]+x[ 3], 7);   
        x[ 2] ^= R(x[ 1]+x[ 0], 9);
        x[ 3] ^= R(x[ 2]+x[ 1],13);   
        x[ 0] ^= R(x[ 3]+x[ 2],18);

        x[ 6] ^= R(x[ 5]+x[ 4], 7);   
        x[ 7] ^= R(x[ 6]+x[ 5], 9);
        x[ 4] ^= R(x[ 7]+x[ 6],13);  
        x[ 5] ^= R(x[ 4]+x[ 7],18);

        x[11] ^= R(x[10]+x[ 9], 7);  
        x[ 8] ^= R(x[11]+x[10], 9);
        x[ 9] ^= R(x[ 8]+x[11],13);  
        x[10] ^= R(x[ 9]+x[ 8],18);

        x[12] ^= R(x[15]+x[14], 7);  
        x[13] ^= R(x[12]+x[15], 9);
        x[14] ^= R(x[13]+x[12],13);  
        x[15] ^= R(x[14]+x[13],18);
    }
    for(i = 0; i < 16; ++i) {
        out[i] = x[i] + in[i];
    }
}

译者注:从上面第二个for循环可以看出处理都是分为4个数一组处理,如上第一次处理分组为{0, 4, 8, 12},{1, 5, 9, 13}, {2, 6, 10, 14}, {3, 7, 11, 15},第二次处理分组为{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}, {12, 13, 14, 15}。所以可以看出第二次处理需要依赖第一次处理的值,很难实现并行


3.scryptBlockMix算法

scryptBlockMix算法,除了它使用了Salsa20/8作为哈希H函数,其他与BlockMix算法基本相同。下面Salsa(T)对应Salsa20/8 Core函数输入是八位向量T。

scryptBlockMix算法
参数:
r 块容量参数(注:只是个参数,不是个数z。ltc协议中r = 1);
输入:
B[0],...,B[2 * r - 1] (2 * r)个64字节块(每个块包含64个字节)输入向量;
输出:
B'[0],...,B'[2 * r - 1] (2 * r)个64字节块(每个块包含64个字节)输出向量;
步骤(伪代码):

1. X = B[2 * r - 1]          //取出最后一个Block     
2. for i = 0 to 2 * r -1 do
       T = X xor B[i]        //将所有块分别与上一个Salsa的结果做异或,第一块则跟最后一个块做异或
       X = Salsa(T)          //算出哈希
       Y[i] = X                //保存
   end for
3.B' = (Y[0], Y[2],..., Y[2 * r - 2],    
          Y[1], Y[3],..., Y[2 * r - 1])    //打乱顺序,因为ltc中r = 1
                                               //B' = (Y[0], Y[1])

4.scryptROMix算法

scryptROMix算法,除了使用scryptBlockMix作为哈希H函数,其他与ROMix算法基本相同,(and the Integerify function explained inline.)
scryptROMix算法
输入:
r 块容量参数(ltc协议中r = 1);
B 长度为(128 * r)字节的输入向量;
N CPU/内存消耗参数,必须大于1, 它是2的n次幂并且小于2^(128 * r / 8)(在ltc协议中 N = 1024);
输出:
B' 长度为(128 * r)字节的输出向量;
步骤:

1. X = B
2. for i = 0 to N - 1 do
       V[i] = X                        //备份一个X,X是一个(128 * r)个字节的数据
       X = scryptBlockMix(X)    //scryptBlockMix算法
   end for                  
3.for i = 0 to N - 1 do          
      j = Integerify(X) mod N   //
          这里 Integerify (B[0] ... B[2 * r - 1])  定义为  将 B[2 * r - 1] 转换为小端整数。在ltc算法中,因为r = 1,推出X = {B[0], B[1]},那么Integerify(X)就是把B[1](后64字节转换成小端)
      T = X xor V[j]
      X = scryptBlockMix(T)    
   end for
4.B' = X

下面代码选自链接
参照上方伪代码分析

static inline void scrypt_core(uint32_t *X, uint32_t *V) {
    uint32_t i, j, k;

    for (i = 0; i < 1024; i++) {          // N = 1024
        memcpy(&V[i * 32], X, 128);    // V[i] = X  注意这里数据按字节写入,128个字节
        xor_salsa8(&X[0], &X[16]);      //
        xor_salsa8(&X[16], &X[0]);      // 这两行属于scryptBlockMix(X),输出顺序没有打乱?
    }
    for (i = 0; i < 1024; i++) {          // N = 1024
        j = 32 * (X[16] & 1023);         // Integerify(X) mod N, 对应之前,数据按字节写入,字节索引要变回字索引,所以要乘以32
        for (k = 0; k < 32; k++) {
            X[k] ^= V[j + k];              // X xor V[j]
        }
        xor_salsa8(&X[0], &X[16]);      //
        xor_salsa8(&X[16], &X[0]);      // 这两行属于scryptBlockMix(X),输出顺序没有打乱?
    }
}

5.scrypt算法

下面所使用的PBKDF2-HMAC-SHA-256函数表示PBKDF2算法以HMAC-SHA-256算法为PRF(注:PRF是PBKDF2中一个可选模块)。HMAC-SHA-256函数生成32字节输出。

scrypt算法
输入:
P 密码短语,一个字节串
S 盐, 一个字节串(在ltc协议中盐是与输入相同的80个字节)
N CPU/内存消耗参数,必须大于1, 是2的n次幂并且小于2^(128 * r / 8)
r 块容量参数
p 并行化参数,一个小于等于((2^32-1) * hLen) / MFLen的正整数,其中hLen为 32,MFlen是128 * r。(在ltc协议中 p = 1)
dkLen 期望输出的派生密钥的字节长度;一个小于等于(2^32 - 1) * hLen的正整数,其中hLen为32
输出:
DK 派生密钥, 长度为dkLen个字节
步骤:

1. B[0] || B[1] || ... || B[p - 1] = PBKDF2-HMAC-SHA256 (P, S, 1, p * 128 * r)
2. for i = 0 to p - 1 do
       B[i] = scryptROMix (r, B[i], N)
    end for
3. DK = PBKDF2-HMAC-SHA256 (P, B[0] || B[1] || ... || B[p - 1], 1, dkLen)

补充:

PBKDF2算法

Password-Based Cryptography Specification 参考资料
PBKDF2(P, S, C, dkLen)
选项:

PRF 内含伪随机数函数(hLen表示伪随机数函数输出的字节长度)
输入:

P 密码,一个字节串;
S 盐,一个字节串;
C 迭代次数,一个正整数;
dkLen 期望的密钥字节长度,一个正整数,最大为(2^32 - 1) hLen;
输出:
DK 派生密钥,一个dkLen长字节串;
步骤:

1.若dkLen > (2^32 - 1) * hLen,输出“派生密钥太长”并停止。
2.l是派生密钥中的hLen字节块个数,取整,然后将上一个块的的字节数赋值给r
l = CEIL(dkLen / hLen),
r = dkLen - (1 - 1) * hLen。
其中,CEIL(x)是“ceiling”函数,往上取整
3.对密码P,盐S,迭代数c和块索引使用下面定义的F函数,从而计算出块
T_1 = F(P, S, c, 1),
T_2 = F(P, S, c, 2),
...
T_1 = F(P, S, c, 1),
其中F函数定义如下:
F(P, S, c, i) = U_1 xor U_2 xor ... xor U_c
U_1 = PRF(P, S || INT(i))
U_2 = PRF(P, U_1)
...
U_c = PRF(P, U_{c - 1})
INT(i)是对整数i四字节编码,高位在前
4.合并所有块并提取前dkLen字节生成派生密钥DK:
DK = T_1 || T_2 || ... || Tl<0..r-1>
5.输出派生密钥DK

注意的是,函数F的结构遵循“吊带”原则("belt-and-suspenders" approach)。U_i是通过迭代的形式计算,从而使攻击者无法通过并行来计算;它们(U_i...)全部异或起来为了避免迭代退化成一个小数组(不明白这句,the recursion degenerating into a small set of values)。

HMAC-SHA-256算法

HMAC-SHA-256的定义
Computes a Hash-based Message Authentication Code (HMAC) by using the SHA256 hash function(用SHA256哈希函数计算基于哈希的消息校验码)

US Secure Hash Algorithms 参考资料
SHA-256 参考资料


6.ASN.1语法


7.Salsa20/8测试向量


8.scryptBlockMix测试向量


9.scryptROMix测试向量


10.PBKDF2-HMAC-SHA-256测试向量


11.Scrypt测试向量

补充小知识:
1.抗冲突性(collision resistance): 它是加密学哈希函数的一个特性。若一个哈希函数具有抗冲突性,那么就很难找到两个输入具有同一个输出。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值