google authenticator 工作原理
2017-08-29 17:49
218 查看
Google authenticator
介绍
Google authenticator是一个基于TOTP原理实现的一个生成一次性密码的工具,用来做双因素登录,市面上已经有很多这些比较成熟的东西存在,像是一些经常用到的U盾,以及数字密码等参考文档
https://tools.ietf.org/html/rfc6238https://tools.ietf.org/html/rfc4226
实现源码 Google authenticator版本
https://github.com/google/google-authenticator-android先来看看google-authenticator-android里面实现的一个版本,基于TOTP:Time-based One-time Password Algorithm(基于时间的一次性密码算法)
1.拿到HMACSHA1计算之后的签名
static Signer getSigningOracle(String secret) { try { //拿到secret base32之后的byte数组 byte[] keyBytes = decodeKey(secret); //使用hmacsha1计算字符串的签名 final Mac mac = Mac.getInstance("HMACSHA1"); mac.init(new SecretKeySpec(keyBytes, "")); // Create a signer object out of the standard Java MAC implementation. return new Signer() { @Override public byte[] sign(byte[] data) { return mac.doFinal(data); } }; } catch (DecodingException error) { .... } return null; } private static byte[] decodeKey(String secret) throws DecodingException { return Base32String.decode(secret); }
2.生成数字码
public String generateResponseCode(long state) throws GeneralSecurityException { byte[] value = ByteBuffer.allocate(8).putLong(state).array(); return generateResponseCode(value); } public String generateResponseCode(byte[] challenge) throws GeneralSecurityException { //通过HMACSHA1拿到签名后的byte数组 byte[] hash = signer.sign(challenge); // Dynamically truncate the hash // OffsetBits are the low order bits of the last byte of the hash //动态拿到offset 0xF 二进制 1111 //offset是去掉高位后的数据 保留后四位 int offset = hash[hash.length - 1] & 0xF; // Grab a positive integer value starting at the given offset. //把hash转换成int // 0x7FFFFFFF 1111111111111111111111111111111 //offset 就是截取过后的hash拿到的Int value int truncatedHash = hashToInt(hash, offset) & 0x7FFFFFFF; //然后根据code length拿到具体需要多少位 int pinValue = truncatedHash % DIGITS_POWER[codeLength]; return padOutput(pinValue); } private int hashToInt(byte[] bytes, int start) { DataInput input = new DataInputStream( new ByteArrayInputStream(bytes, start, bytes.length - start)); int val; try { val = input.readInt(); } catch (IOException e) { throw new IllegalStateException(e); } return val; }
3.流程
1.客户端和服务端使用HMACSHA1 这个加密算法进行加密,首先客户端和服务端会商量一个key,然后把这个key Base32之后当作算法的加密key2.然后客户端和服务端需要维持一个
long state一个值,如果服务端和客户端不能够通信,那么其实用时间当作这个state即可,当然这个得保证估计几十秒的容错性,一旦时间误差比较大就会验证不通过,这个就是TOTP的缺点。
3.双方通过加密这个
long state得到统一加密后的数据
byte[] hash,首先取
byte[] hash的最后一个byte& 0xF,那么就是去掉高位,留下一个小于15的数字
offset,然后通过截取
hash[offset:length],这部分byte[]数组转换成数字
truncatedHash,最后根据设置的返回码的位数,来决定取
truncatedHash中的多少位。
整个算法流程就是上面这三步,其实没有什么高大上的东西,这里面有一个难点,就是保证时间的容错性,
如何保证时间的容错性?
mTotpCounter.getValueAtTime(Utilities.millisToSeconds(mTotpClock.currentTimeMillis()));
首先是
mTotpClock.currentTimeMillis()拿到当前毫秒的时间戳,然后是
Utilities.millisToSeconds把毫秒转换成秒,最后是
mTotpCounter.getValueAtTime,这里做了时间的容错
public long getValueAtTime(long time) { assertValidTime(time); // According to the RFC: // T = (Current Unix time - T0) / X, where the default floor function is used. // T - counter value, // T0 - start time. // X - time step. // It's important to use a floor function instead of simple integer division. For example, // assuming a time step of 3: // Time since start time: -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 // Correct value: -2 -2 -2 -1 -1 -1 0 0 0 1 1 1 2 // Simple division / 3: -2 -1 -1 -1 0 0 0 0 0 1 1 1 2 // // To avoid using Math.floor which requires imprecise floating-point arithmetic, we // we compute the value using integer division, but using a different equation for // negative and non-negative time since start time. long timeSinceStartTime = time - mStartTime; if (timeSinceStartTime >= 0) { return timeSinceStartTime / mTimeStep;//mTimeStep 这里默认是30 } else { return (timeSinceStartTime - (mTimeStep - 1)) / mTimeStep; } }
返回的是
timeSinceStartTime / mTimeStep;
mTimeStep这里默认是30,也就是说可以忽略30秒的延迟
生成code的流程以及原因
参照https://tools.ietf.org/html/rfc4226第一步是通过HMAC-SHA-1生成一个长度为20byte数组 Step 1: Generate an HMAC-SHA-1 value Let HS = HMAC-SHA-1(K,C) // HS is a 20-byte string 第二步是通过动态截取之后返回的一个31位的string,也就是长度为4的byte[] Step 2: Generate a 4-byte string (Dynamic Truncation) Let Sbits = DT(HS) // DT, defined below, // returns a 31-bit string 第三步byte[]转换成整数 Step 3: Compute an HOTP value Let Snum = StToNum(Sbits) // Convert S to a number in 0...2^{31}-1 //最后根据自己需要code的位数,截取整数的一部分 Return D = Snum mod 10^Digit // D is a number in the range 0...10^{Digit}-1
核心在第二步的DT函数
DT(String) // String = String[0]...String[19] Let OffsetBits be the low-order 4 bits of String[19] Offset = StToNum(OffsetBits) // 0 <= OffSet <= 15 Let P = String[OffSet]...String[OffSet+3] Return the Last 31 bits of P
先来看看OffsetBits是如何计算出来的
int offset = hash[hash.length - 1] & 0xF;
其实就是取byte数组的最后一个byte也就是byte[19]然后跟0xF做&运算,运算之后得到的一定是一个
0<=offset<=15的数,而为什么要这个区间的一个数呢?首选是一个int可以用4个byte来存储,其次总的byte的长度是20,最大15+4刚好取到byte[19]没有超过。
最后是拿到一个只有31位的整数,来看看代码是如何计算的
int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
其他都很正常取了四个字节,转换成整数,然是注意看最高位
(hash[offset] & 0x7f) << 24)这里从
0xff变成了
0x7f也就是127(1111111),二进制少了1位,也就是把高位干掉了,32位变成了31位,为什么要使用31位呢?rfc文档解释如下:
The reason for masking the most significant bit of P is to avoid confusion about signed vs. unsigned modulo computations. Different processors perform these operations differently, and masking out the signed bit removes all ambiguity.
用中文来解释大概意思就是关于符号与无符号模运算的混淆。不同
处理器以不同的方式执行这些操作,并屏蔽掉
符号位消除所有歧义。大概就是这个意思。
扩展
原理想通的算法还有一个:HOTP(基于计数器)
相关文章推荐
- Spring 工作原理
- [翻译文章]: 关于HEVC工作原理的介绍 (上)
- SpringMVC工作原理
- MVC的工作原理
- SSL的工作原理
- HashMap的工作原理
- HTTP详解(1)-工作原理
- Android系统Recovery工作原理之使用update.zip升级过程分析(五)---update.zip包从上层进入Recovery服务
- 多播 - 理解IP多播的工作原理
- 量子计算机的工作原理(转)
- (转)Struts2的工作原理
- Android——View的工作原理(三)
- Java ArrayList工作原理及实现
- HashMap的工作原理(转)
- cookie的工作原理
- 74HC595引脚图时序图工作原理及pdf中文资料
- gstreamer插件工作原理与流程分析 .
- HashMap工作原理整理
- Spark基本工作原理与RDD
- 类加载器的工作原理