您的位置:首页 > 编程语言 > Go语言

google authenticator 工作原理

2017-08-29 17:49 218 查看

Google authenticator

介绍

Google authenticator是一个基于TOTP原理实现的一个生成一次性密码的工具,用来做双因素登录,市面上已经有很多这些比较成熟的东西存在,像是一些经常用到的U盾,以及数字密码等

参考文档

https://tools.ietf.org/html/rfc6238

https://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之后当作算法的加密key

2.然后客户端和服务端需要维持一个
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(基于计数器)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息