君は春の中にいる、かけがえのない春の中にいる.

你驻足于春色中,于那独一无二的春色之中.

Harbor中的用户密码加密机制探究

我们一般自己写个小程序做用户验证的时候,大多数情况都是用MD5,sha1,好一点的再加个盐,这次让我们来看看开源软件中的用户密码加密都是怎么实现的。

0x01 Harbor简介

Project Harbor是由VMware公司中国团队为企业用户设计的Registry server开源项目。主要用于Docker镜像仓库的管理。

嗯,简介就到这里。

0x02 算法探究

在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个输入参数,如下:

1
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有代码段如下:

1
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实现,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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]

调用相关函数进行测试,

1
2
3
4
5
6
7
8
9
10
11
12
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')

得到了和数据库中相同的结果。

0x03 结束语

上面提出的Hash密码的算法在国外普遍被接受并运用于密码保护中,可以有效抵抗彩虹表爆破,但是国内这方面的加密往往还是用最基本的几种方式。在今后的程序编写中,可以尝试使用这些更安全的Hash算法。