除了加盐来抵御rainbow table 攻击之外,bcrypt的一个非常主要的特色便是自适应性,可以担保加密的速率在一个特定的范围内,纵然打算机的运算能力非常高,可以通过增加迭代次数的办法,使得加密速率变慢,从而可以抵御暴力搜索攻击。
bcrypt函数是OpenBSD和其他系统包括一些Linux发行版(如SUSE Linux)的默认密码哈希算法。
bcrypt的事情事理我们先回顾一下Blowfish的加密事理。 blowfish首先须要天生用于加密利用的K数组和S-box, blowfish在天生终极的K数组和S-box须要耗费一定的韶光,每个新的密钥都须要进行大概4 KB文本的预处理,和其他分组密码算法比较,这个会很慢。但是一旦天生完毕,或者说密钥不变的情形下,blowfish还是很快速的一种分组加密方法。
那么慢有没有好处呢?
当然有,由于对付一个正常运用来说,是不会常常改换密钥的。以是预处理只会天生一次。在后面利用的时候就会很快了。
而对付恶意攻击者来说,每次考试测验新的密钥都须要进行漫长的预处理,以是对攻击者来说要破解blowfish算法是非常不划算的。以是blowfish是可以抵御字典攻击的。
Provos和Mazières利用了这一点,并将其进一步发展。他们为Blowfish开拓了一种新的密钥设置算法,将由此产生的密码称为 “Eksblowfish”(”expensive key schedule Blowfish”)。这是对Blowfish的改进算法,在bcrypt的初始密钥设置中,salt 和 password 都被用来设置子密钥。然后经由一轮轮的标准Blowfish算法,通过交替利用salt 和 password作为key,每一轮都依赖上一轮子密钥的状态。虽然从理论上来说,bcrypt算法的强度并不比blowfish更好,但是由于在bcrpyt中重置key的轮数是可以配置的,以是可以通过增加轮数来更好的抵御暴力攻击。
bcrypt算法实现大略点说bcrypt算法便是对字符串OrpheanBeholderScryDoubt 进行64次blowfish加密得到的结果。有朋友会问了,bcrypt不是用来对密码进行加密的吗?怎么加密的是一个字符串?
别急,bcrpyt是将密码作为对该字符串加密的因子,同样也得到了加密的效果。我们看下bcrypt的基本算法实现:
Function bcrypt Input: cost: Number (4..31) log2(Iterations). e.g. 12 ==> 212 = 4,096 iterations salt: array of Bytes (16 bytes) random salt password: array of Bytes (1..72 bytes) UTF-8 encoded password Output: hash: array of Bytes (24 bytes) //Initialize Blowfish state with expensive key setup algorithm //P: array of 18 subkeys (UInt32[18]) //S: Four substitution boxes (S-boxes), S0...S3. Each S-box is 1,024 bytes (UInt32[256]) P, S <- EksBlowfishSetup(cost, salt, password) //Repeatedly encrypt the text "OrpheanBeholderScryDoubt" 64 times ctext <- "OrpheanBeholderScryDoubt" //24 bytes ==> three 64-bit blocks repeat (64) ctext <- EncryptECB(P, S, ctext) //encrypt using standard Blowfish in ECB mode //24-byte ctext is resulting password hash return Concatenate(cost, salt, ctext)
上述函数bcrypt 有3个输入和1个输出。
在输入部分,cost 表示的是轮循的次数,这个我们可以自己指定,轮循次数多加密就慢。
salt 是加密用盐,用来稠浊密码利用。
password 便是我们要加密的密码了。
末了的输出是加密后的结果hash。
有了3个输入,我们会调用EksBlowfishSetup函数去初始化18个subkeys和4个1K大小的S-boxes,从而达到终极的P和S。
然后利用P和S对”OrpheanBeholderScryDoubt” 进行64次blowfish运算,终极得到结果。
接下来看下 EksBlowfishSetup方法的算法实现:
Function EksBlowfishSetup Input: password: array of Bytes (1..72 bytes) UTF-8 encoded password salt: array of Bytes (16 bytes) random salt cost: Number (4..31) log2(Iterations). e.g. 12 ==> 212 = 4,096 iterations Output: P: array of UInt32 array of 18 per-round subkeys S1..S4: array of UInt32 array of four SBoxes; each SBox is 256 UInt32 (i.e. 1024 KB) //Initialize P (Subkeys), and S (Substitution boxes) with the hex digits of pi P, S <- InitialState() //Permutate P and S based on the password and salt P, S <- ExpandKey(P, S, salt, password) //This is the "Expensive" part of the "Expensive Key Setup". //Otherwise the key setup is identical to Blowfish. repeat (2cost) P, S <- ExpandKey(P, S, 0, password) P, S <- ExpandKey(P, S, 0, salt) return P, S
代码很大略,EksBlowfishSetup 吸收上面我们的3个参数,返回终极的包含18个子key的P和4个1k大小的Sbox。
首先初始化,得到最初的P和S。
然后调用ExpandKey,传入salt和password,天生第一轮的P和S。
然后循环2的cost方次,轮流利用password和salt作为参数去天生P和S,末了返回。
末了看一下ExpandKey的实现:
Function ExpandKey Input: password: array of Bytes (1..72 bytes) UTF-8 encoded password salt: Byte[16] random salt P: array of UInt32 Array of 18 subkeys S1..S4: UInt32[1024] Four 1 KB SBoxes Output: P: array of UInt32 Array of 18 per-round subkeys S1..S4: UInt32[1024] Four 1 KB SBoxes //Mix password into the P subkeys array for n <- 1 to 18 do Pn <- Pn xor password[32(n-1)..32n-1] //treat the password as cyclic //Treat the 128-bit salt as two 64-bit halves (the Blowfish block size). saltHalf[0] <- salt[0..63] //Lower 64-bits of salt saltHalf[1] <- salt[64..127] //Upper 64-bits of salt //Initialize an 8-byte (64-bit) buffer with all zeros. block <- 0 //Mix internal state into P-boxes for n <- 1 to 9 do //xor 64-bit block with a 64-bit salt half block <- block xor saltHalf[(n-1) mod 2] //each iteration alternating between saltHalf[0], and saltHalf[1] //encrypt block using current key schedule block <- Encrypt(P, S, block) P2n <- block[0..31] //lower 32-bits of block P2n+1 <- block[32..63] //upper 32-bits block //Mix encrypted state into the internal S-boxes of state for i <- 1 to 4 do for n <- 0 to 127 do block <- Encrypt(state, block xor salt[64(n-1)..64n-1]) //as above Si[2n] <- block[0..31] //lower 32-bits Si[2n+1] <- block[32..63] //upper 32-bits return state
ExpandKey紧张用来天生P和S,算法的天生比较繁芜,大家感兴趣的可以详细研究一下。
bcrypt hash的构造我们可以利用bcrypt来加密密码,终极以bcrypt hash的形式保存到系统中,一个bcrypt hash的格式如下:
$2b$[cost]$[22 character salt][31 character hash]
比如:
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy\__/\/ \____________________/\_____________________________/ Alg Cost Salt Hash
上面例子中,$2a$ 表示的hash算法的唯一标志。这里表示的是bcrypt算法。
10 表示的是代价因子,这里是2的10次方,也便是1024轮。
N9qo8uLOickgx2ZMRZoMye 是16个字节(128bits)的salt经由base64编码得到的22长度的字符。
末了的IjZAgcfl7p92ldGxad68LJZdL17lhWy是24个字节(192bits)的hash,经由bash64的编码得到的31长度的字符。
hash的历史这种hash格式是遵照的是OpenBSD密码文件中存储密码时利用的Modular Crypt Format格式。最开始的时候格式定义是下面的:
$1$: MD5-based crypt (‘md5crypt’)$2$: Blowfish-based crypt (‘bcrypt’)$sha1$: SHA-1-based crypt (‘sha1crypt’)$5$: SHA-256-based crypt (‘sha256crypt’)$6$: SHA-512-based crypt (‘sha512crypt’)但是最初的规范没有定义如何处理非ASCII字符,也没有定义如何处理null终止符。修订后的规范规定,在hash字符串时:
String 必须是UTF-8编码必须包含null终止符由于包含了这些改动,以是bcrypt的版本号被修正成了 $2a$。
但是在2011年6月,由于PHP对bcypt的实现 crypt_blowfish 中的一个bug,他们建议系统管理员更新他们现有的密码数据库,用$2x$代替$2a$,以表明这些哈希值是坏的(须要利用旧的算法)。他们还建议让crypt_blowfish对新算法天生的哈希值利用头$2y$。 当然这个改动只限于PHP的crypt_blowfish。
然后在2014年2月,在OpenBSD的bcrypt实现中也创造了一个bug,他们将字符串的长度存储在无符号char中(即8位Byte)。如果密码的长度超过255个字符,就会溢出来。
由于bcrypt是为OpenBSD创建的。以是当他们的库中涌现了一个bug时, 他们决定将版本号升级到$2b$。
本文已收录于 http://www.flydean.com/37-bcrypt/
最普通的解读,最深刻的干货,最简洁的教程,浩瀚你不知道的小技巧等你来创造!
欢迎关注我的公众年夜众号:「程序那些事」,懂技能,更懂你!