RFC6328: Time Based One-Time Password

Last renew: April 7, 2022 pm

RFC6328: Time Based One-Time Password (TOTP)

注:本文仅为个人学习笔记,无任何版权。

最近做到的课题需要我利用RFC6328将自己的邮箱信息加密,作为账号密码发送给对方的服务器。中文全网对于RFC6328的文章较少,在此稍微记载下学习的过程与心得。

Go写的TOTP库 gototp

TOTP是什么?

TOTP指基于时间的一次性密码。TOTP是基于OTP (One-Time Password, 一次性密码)的扩展。在RFC4226中,定义了一种基于HMAC的一次性密码,被命名为HOTP。HOTP算法是基于事件的,对其来说移动因子是一个事件计数器。而在TOTP当中,移动因子建立在了一个时间值上。该算法通过利用时间值,提供了一个短命的OTP值,可以有效提高安全性。

HOTP算法

HOTP(HMAC-based One-Time Password, RFC4226)算法是基于HMAC-SHA-1算法的,该算法公式如下所示。

HOTP(K, C) = Truncate(HMAC-SHA-1(K, C))

其中Truncate代表可以将HMAC-SHA-1转换为HOTP值的函数,K代表共享的秘密,C代表计数器值。

换言之,想要得到HOTP密码,有如下几个先决条件:

  1. 知道Truncate函数。
  2. 服务器端与客户端有共享的秘密。
  3. 用相同的计数器方法。

HOTP算法与TOTP算法的区别

TOTP算法是HOTP算法基于时间的变体。在TOTP算法中,计数器值C被替换为了由时间参考和时间步长得出的值T。首先你要输入现在的Unixtime,然后减去所设置的T0。实际情况下将时间精确到每秒是无法实现的,一般会提供一个步长。

TOTP可以使用HMAC-SHA-256/512函数,不同于HOTP算法的HMAC-SHA-1函数

TOTP算法要求

R1: The prover (e.g., token, soft token) and verifier (authentication or validation server) MUST know or be able to derive the current Unix time (i.e., the number of seconds elapsed since midnight UTC of January 1, 1970) for OTP generation. See [UT] for a more detailed definition of the commonly known “Unix time”. The precision of the time used by the prover affects how often the clock synchronization should be done; see Section 6.

(服务器端与客户端均能得知现在的Unix时间)

R2: The prover and verifier MUST either share the same secret or the knowledge of a secret transformation to generate a shared secret.

(服务器端与客户端需要共享相同的秘密/共享产生共享秘密的转换的知识)

R3: The algorithm MUST use HOTP [RFC4226] as a key building block.

(算法必须使用HOTP算法来构建)

R4: The prover and verifier MUST use the same time-step value X.

(服务器端与客户端必须使用相同的步长X)

R5: There MUST be a unique secret (key) for each prover.

(每个验证者必须有一个独特的秘密(密钥))

R6: The keys SHOULD be randomly generated or derived using key derivation algorithms.

(密钥应当是随机生成/使用推导算法)

R7: The keys MAY be stored in a tamper-resistant device and SHOULD be protected against unauthorized access and usage.

(密钥应当储存在安全设备中)

TOTP算法详解

符号

K代表服务器端与客户端共同了解的秘密。

X代表以秒为单位的时间步长(默认值X=30秒),是一个系统参数。

T0是开始计算时间步长的Unix时间(缺省值为0,即Unix epoch,1970年1月1日),也是一个系统参数。

算法细节

我们将TOTP定义为HOTP(K, T), T是一个整数,代表初始计数器时间T0和当前Unix时间之间的时间步数。

TOTP(K, T0, X) = HOTP(K, T(T0, X))

T(T0, X) = (CurrentUnixTime - T0) / X

TOTP算法实装

1
2
3
4
5
6
7
8
9
package gototp

func TOTP(k string, t0, x int) int{
return HOTP(k, T(T0, x))
}

func T(t0, x int) int {
return (time.Now().Unix - t0)/x
}

上述代码中,我们可以发现HOTP函数并未被定义。所以我们还需要对于HOTP进行定义。

HOTP算法实装

想要求出HOTP,其实大体只需要三步。

  1. 用Key与Count求出HMAC-SHA1的值。
  2. 生成4个字节的字符串
  3. 计算HOTP值

我们来逐一分析一下

1. 求解HMAC-SHA1的值

HMAC是密钥相关的哈希计算消息认证码(Hash-based Message Authentication Code)在多种网络协议(如SSL)中得到了广泛使用。通过使用哈希函数,可以从Key和Message中生成认证码。

假设HMAC-SHA1的值为HS,计算公式如下所示:

HS = HMAC-SHA-1(K, C)

K:共同秘密key

C:Message,在HOTP函数中为count中的值

对于Golang来说,HMAC和SHA1都有标准包可以引用,HMAC为crypto/hmac、SHA1为crypto/sha1

1
2
3
4
5
func HMACSHA1(k, c []byte) []byte{
mac := hmac.New(sha1.New, k)
mac.Write(c)
return mac.Sum(nil)
}

对于Golang来说,HMAC是用has.Hash实装的,利用hmac.New(func()has.Hash, []byte)可以得到哈希函数对应的对象。这样我们就成功的计算出了HS值。

注;对于SHA1来说,存储空间最大为160bit,最大支持20文字的字符串。

2. 生成一个4byte的字符串

下一步,则是利用第一步计算出的HMAC值,制作一个4byte长度的字符串。

Sbits = DT(HS)

首先计算offsetbits,这是HS的20个字中最后的4bit。

由于Go语言处理的是字节串(Byte String),所以当我取出20个字的最后一个字时,取出的其实是一个8bit的数字,由于我们只需要最后的4bit,所以一般情况下给到的结果是(0000xxxx)前四位bit被屏蔽了。同时,由于go没有二进制的字符,所以我们实际上表示会使用16进制。

1
2
3
offsetbits := hs[19] & 0xF
// 0x后面的数为16进制
// & 位and运算 同1为1

下一步求解offset

offset可以将offsetbits作为数值取出。

并没有什么特别的变换,仅仅是直接将byte列作为Integer读出。(因为offsetbits是4bit,所以offset这个值为0-15)

1
offset := int(offsetbits)

接下来,将HS的第offset位开始之后4个文字拿出来,赋值给p

1
p := hs[offset:offset + 4]

然后提取p(32bit)末尾的31bit(0xx…..xx(31个x))

这也是利用mask来做计算的,换句话说与7FFFFFFF做and运算就可以。

当然,由于用byte列做mask会稍微有些麻烦,所以可以先将[]byte变换为int然后做mask。

由于Go语言不支持直接变换,所以借用一个包encoding/binary

总结一下第二步所做的工作。

1
2
3
4
5
6
7
// Truncate 函数
func DT(hs []byte) int {
offsetbits := hs[19] & 0xF
offset := int(offsetbits)
p := hs[offset : offset+4]
return int(binary.BigEndian.Uint32(p)) & 0x7FFFFFFF
}

3. 计算HOTP的值

1
2
3
func ReductionModulo(snum int) int {
return int(int64(snum) % int64(math.Pow10(g.Digit)))
}

HOTP算法总结

最后将HOTP函数构建一下,将上面的步骤套入。

再次回顾一下HOTP函数

HOTP(K, C) = Truncate(HMAC - SHA- 1(K, C))

Truncate函数是用上面的DT实现的

1
2
3
func HOTP(k, c []byte) int{
return DT(HMACSHA1(k, c))
}

联合TOTP和HOTP

完成HOTP函数的构建之后,我们要想办法将其应用在TOTP之中。

之前我们构建的TOTP函数如下所示

1
2
3
4
5
6
7
func TOTP(k string, t0, x int) int {
return HOTP(k, T(T0, x))
}

func T(t0, x int) int {
return (time.Now().Unix - t0) / x
}

实际上,T(t0, x)的返回值是int,是无法在func HOTP(k, c []byte) int中使用的,所以要对HOTP函数进行微调。

1
2
3
4
5
func HOTP(k []byte, c int) int{
cb := make([]byte, 8)
binary.BigEndian.PutUint64(cb, c)
return DT(HMACSHA1(k, cb))
}

这样,我们完成了TOTP算法的基础实现。