我们一般自己写个小程序做用户验证的时候,大多数情况都是用MD5,sha1,好一点的再加个盐,这次让我们来看看开源软件中的用户密码加密都是怎么实现的。
Project Harbor是由VMware公司中国团队为企业用户设计的Registry server开源项目。主要用于Docker镜像仓库的管理。
嗯,简介就到这里。
在Harbor的数据库中看到了这样一条用户验证的存储信息。
| username | password | salt | | test | 65e900b5a2bdff474e29d0d2b21f4945 | gktqer4zml32472wmht9xeuixvg5pvjd |
test账户的明文密码为 123QWEqwe
看到如上的数据库结构,32位的散列,猜测多半就是MD5(password+salt)的加密套路,然而事实证明我还是太年轻,无论我如何改变密码和盐的组合,都无法得到数据库中的密码散列,为了节省大家尝试的时间,我就把几种组合方式的结果罗列在下面:
MD5(p+s): 7bd52852dd48c4375aa29bd73e125183
MD5(s+p): c01fb693df3c524442149ff16d7d5fc8
MD5(MD5(p)+s): 87b9168b430edb9fcfd03474c7f35ac0
MD5(MD5(s)+p): d70ffc1ace99fa8d7e52ef3e29907a54
常识性的MD5猜测竟然都没有正确,那么可能的加密方式就难以捉摸了,SHA1后截断?,HMAC?
首先想到的是去Harbor的官档里找答案,不过大概翻阅了可能的官方文档后,并没有找到对它加密方式的记录。但是在Github的一个issue中,有许多人关于PBKDF2的讨论,会不会是使用这种加密方式呢?
通过查阅资料得知(中文维基竟然没有关于PBKDF2的解释条目,我的姿势不对?),PBKDF2是一种基于密码的密钥生成函数。
这种算法有5个输入参数,如下:
DK = PBKDF2(PRF, Password, Salt, c, dkLen)
PRF是一个伪随机函数,Password是主密码,Salt是盐值,c是算法迭代次数,dkLen是产生密钥的长度。
那么到这里,我们就需要去读Harbor的源代码来获取算法中的几个常数参数来验证我们的思路。
Harbor中的代码主要有两种语言构成,Go和AngularJS,这两种都是我没有实践过的语言,定位它的加密函数花费了不少时间。
首先我们找到Harbor src源码中ui的main.go,可以发现其中有关于密码方面的操作调用了dao这个包,而在dao包中有一个文件叫user.go,其中有个函数LoginByDb,LoginByDb有代码段如下:
if user.Password != utils.Encrypt(auth.Password, user.Salt)
可以得知,它的加密用的是utils中的Encrypt。
再跟踪Encrypt函数
func Encrypt(content string, salt string) string { return fmt.Sprintf("%x", pbkdf2.Key([]byte(content), []byte(salt), 4096, 16, sha1.New)) }
果然,Harbor确实使用了pbkdf2算法,调用的Hash函数为Sha1,迭代4096次,密钥长度为int型16位。
为了验证方便,我在github上 https://github.com/mitsuhiko/python-pbkdf2/blob/master/pbkdf2.py 找到了一个pbkdf2的python实现,主要代码如下:
import hmac import hashlib from struct import Struct from operator import xor from itertools import izip, starmap _pack_int = Struct('>I').pack def pbkdf2_hex(data, salt, iterations=4096, keylen=16, hashfunc=None): return pbkdf2_bin(data, salt, iterations, keylen, hashfunc).encode('hex') def pbkdf2_bin(data, salt, iterations=4096, keylen=16, hashfunc=None): hashfunc = hashfunc or hashlib.sha1 mac = hmac.new(data, None, hashfunc) def _pseudorandom(x, mac=mac): h = mac.copy() h.update(x) return map(ord, h.digest()) buf = [] for block in xrange(1, -(-keylen // mac.digest_size) + 1): rv = u = _pseudorandom(salt + _pack_int(block)) for i in xrange(iterations - 1): u = _pseudorandom(''.join(map(chr, u))) rv = starmap(xor, izip(rv, u)) buf.extend(rv) return ''.join(map(chr, buf))[:keylen]
调用相关函数进行测试,
def check(data, salt, iterations, keylen, expected): rv = pbkdf2_hex(data, salt, iterations, keylen) if rv == expected: print 'Test Successful:' print ' Expected: %s' % expected print ' Got: %s' % rv print ' Parameters:' print ' data=%s' % data print ' salt=%s' % salt print ' iterations=%d' % iteration check('123QWEqwe', 'gktqer4zml32472wmht9xeuixvg5pvjd', 4096, 16, '65e900b5a2bdff474e29d0d2b21f4945')
得到了和数据库中相同的结果。