HMAC 算法
《NodeJS开发教程14-Crypto加密与解密》 - 简书
HMAC: 就是带个密码(key)也叫salt的hash算法, key 是发送方和接收方都有的
hmac = md5(message + salt)
Crypto
1.内容编解码类(Base64)
2.内容摘要类(MD5、SHA1、SHA256、SHA512)
3.内容加密解密类 又分为:对称加密解密(AES),非对称加密解密(RSA)
4.内容签名类(RSA+SHA1 或 RSA+SHA256 或 RSA+MD5等等)
如果要通过同一个渠道发送数据和散列值的话(比如消息认证码),就要考虑数据和MD5同时被篡改的问题,如果第三方修改了数据,然后进行MD5散列,并一块发给接收方,接收方并不能察觉到数据被篡改。HMAC-MD5就可以用一把发送方和接收方都有的key进行计算,而没有这把key的第三方是无法计算出正确的散列值的,这样就可以防止数据被篡改。
hmac
通过哈希算法,我们可以验证一段数据是否有效,方法就是对比该数据的哈希值,例如,判断用户口令是否正确,我们用保存在数据库中的password_md5对比计算md5(password)的结果,如果一致,用户输入的口令就是正确的。
为了防止黑客通过彩虹表根据哈希值反推原始口令,在计算哈希的时候,不能仅针对原始输入计算,需要增加一个salt来使得相同的输入也能得到不同的哈希,这样,大大增加了黑客破解的难度。
如果salt是我们自己随机生成的,通常我们计算MD5时采用md5(message + salt)。但实际上,把salt看做一个“口令”,加salt的哈希就是:计算一段message的哈希时,根据不通口令计算出不同的哈希。要验证哈希值,必须同时提供正确的口令。
这实际上就是Hmac算法:Keyed-Hashing for Message Authentication。它通过一个标准算法,在计算哈希的过程中,把key混入计算过程中。
和我们自定义的加salt算法不同,Hmac算法针对所有哈希算法都通用,无论是MD5还是SHA-1。采用Hmac替代我们自己的salt算法,可以使程序算法更标准化,也更安全。
Python自带的hmac模块实现了标准的Hmac算法。我们来看看如何使用hmac实现带key的哈希。
我们首先需要准备待计算的原始消息message,随机key,哈希算法,这里采用MD5,使用hmac的代码如下:
>>> import hmac
>>> message = b'Hello, world!'
>>> key = b'secret'
>>> h = hmac.new(key, message, digestmod='MD5')
>>> # 如果消息很长,可以多次调用h.update(msg)
>>> h.hexdigest()
'fa4ee7d173f2d97ee79022d1a7355bcf'
可见使用hmac和普通hash算法非常类似。hmac输出的长度和原始哈希算法的长度一致。需要注意传入的key和message都是bytes类型,str类型需要首先编码为bytes。
google Authenticator 双因子认证原理
签署:
签署所使用的方法是HMAC-SHA1。HMAC的全称是Hash-based message authentication code(哈希运算消息认证码),以一个密钥和一个消息为输入,生成一个消息摘要作为输出,这里以SHA1作为消息输入。使用HMAC的原因是:只有用户本身知道正确的输入密钥,因此会得到唯一的输出。其算法可以简单表示为:
hmac = SHA1(secret + SHA1(secret + input))
事实上,TOTP是HMAC-OTP(基于HMAC的一次密码生成)的超集,区别是TOTP以当前时间作为输入,而HMAC-OTP以自增计算器作为输入,该计数器使用时需要进行同步。
算法:
首先,要进行密钥的base32加密。虽然谷歌上的密钥格式是带空格的,不过base32拒绝空格输入,并只允许大写。所以要作如下处理:
original_secret = xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
secret = BASE32_DECODE(TO_UPPERCASE(REMOVE_SPACES(original_secret)))
第二步要获取当前时间值,这里使用的是UNIX time函数,或者可以用纪元秒。
input = CURRENT_UNIX_TIME()
在Google Authenticator中,input值拥有一个有效期。因为如果直接根据时间进行计算,结果将时刻发生改变,那么将很难进行复用。Google Authenticator默认使用30秒作为有效期(时间片),最后input的取值为从Unix epoch(1970年1月1日 00:00:00)来经历的30秒的个数。
input = CURRENT_UNIX_TIME() / 30
最后一步是进行HMAC-SHA1运算
original_secret = xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
secret = BASE32_DECODE(TO_UPPERCASE(REMOVE_SPACES(original_secret)))
input = CURRENT_UNIX_TIME() / 30
hmac = SHA1(secret + SHA1(secret + input))
至此,2FA所需的两个因子都已准备就绪了。但是HMAC运算后的结果会是20字节即40位16进制数,应该没有人会愿意每次都输入这么长的密码。我们需要的是常规6位数字密码!
要实现这个愿望,首先要对20字节的SHA1进行瘦身。我们把SHA1的最后4个比特数(每个数的取值是0~15)用来做索引号,然后用另外的4个字节进行索引。因此,索引号的操作范围是15+4=19,加上是以零开始,所以能完整表示20字节的信息。4字节的获取方法是
four_bytes = hmac[LAST_BYTE(hmac):LAST_BYTE(hmac) + 4]
然后将它转化为标准的32bit无符号整数(4 bytes = 32 bit):
large_integer = INT(four_bytes)
最后再进行7位数(1百万)取整,就可得到6位数字了:
large_integer = INT(four_bytes)
small_integer = large_integer % 1,000,000
这也是我们最后要的目标结果,整个过程总结如下:
original_secret = xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
secret = BASE32_DECODE(TO_UPPERCASE(REMOVE_SPACES(original_secret)))
input = CURRENT_UNIX_TIME() / 30
hmac = SHA1(secret + SHA1(secret + input))
four_bytes = hmac[LAST_BYTE(hmac):LAST_BYTE(hmac) + 4]
large_integer = INT(four_bytes)
small_integer = large_integer % 1,000,000
HMAC 算法主要应用于身份验证,用法如下:
1.客户端发出登录请求
2.服务器返回一个随机值,在会话记录中保存这个随机值
3.客户端将该随机值作为密钥,用户密码进行 hmac 运算,递交给服务器
4.服务器读取数据库中的用户密码,利用密钥做和客户端一样的 hmac运算,然后与用户发送的结果比较,如果一致,则用户身份合法。
JWT 所使用的HMAC
alg 是是所使用的 hash 算法例如 HMAC SHA256 或 RSA,typ 是 Token 的类型自然就是 JWT。
{
"alg": "HS256",
"typ": "JWT"
}
我们也常把MAC称为HMAC(keyed-Hash Message Authentication Code)。
MAC(Message Authentication Code,消息认证码算法)是含有密钥的散列函数算法,兼容了MD和SHA算法的特性,并在此基础上加入了密钥。
MAC算法集合了MD和SHA算法的优势,并加入密钥的支持,是一种更为安全的消息摘要算法。
MD系列的算法有 HmacMD2、HmacMD4、HmacMD5 三种算法;
SHA系列的算法有 HmacSHA1、HmacSHA224、HmacSHA256、HmacSHA384、HmacSHA512 五种算法。
可以简单的理解为: 1. MD5或者SHA提取摘要 2.用MAC进行秘钥加密
这么说是没有错的,而我要补充说明的是这里的秘钥问题,因为HMAC的本意秘钥是要随机生成的,而不是客户端与服务器之前约定好的。
HMAC在实践中它应该是这么用的:
(1)客户端向服务器发起请求,访问登录页面.这时服务器生成一个密钥,把这个密钥存储在session之中,然后将密钥返回给客户端.
(2)客户端填写登录表单,点击提交后,运行HMAC算法,根据密钥将用户信息加密后post到服务器.
(3)服务器读取数据库中的密码,用HMAC将密码和session中的密钥进行加密产生密码的密文,将密文与用户提交的进行比较.
但是我看到大多数的实现都没有随机生成秘钥,而是实现约定好秘钥,在客户端与服务端使用,这样做把HMAC的安全性打了折扣。
这里给出以下HMAC的核心实现,不管秘钥是约定好的,还是随机生成的,都使用到了如下的核心方法
具体的使用还是要看具体情况
public static String encodeHmacSHA1(byte[] data, byte[] key) throws Exception {
SecretKey secretKey = new SecretKeySpec(key, "HmacSHA1");
Mac mac = Mac.getInstance(secretKey.getAlgorithm());
mac.init(secretKey);
byte[] digest = mac.doFinal(data);
return new HexBinaryAdapter().marshal(digest);
//转为十六进制的字符串输出
}
//生成秘钥
public static byte[] initHmacSHA1Key() throws NoSuchAlgorithmException {
KeyGenerator generator = KeyGenerator.getInstance("HmacSHA1"); //假设使用到的是HmacSHA1方法
SecretKey secretKey = generator.generateKey();
byte[] key = secretKey.getEncoded();
return key;
}