如何处理基于时间的一次性密码的 26 字节秘密?
Posted
技术标签:
【中文标题】如何处理基于时间的一次性密码的 26 字节秘密?【英文标题】:How to Handle 26-Byte Secret for Time-based One Time Password? 【发布时间】:2022-01-20 03:37:13 【问题描述】:基于时间的一次性密码的秘密通常是 16 字节的 base32 编码字符串。例如GitHub 2FA。
但在某些情况下,它有 26 个字节长。例如Tutanota OTP。通常是带有空格的小写字母,例如:vev2 qjea un45 3sr4 q4h3 ais4 ci
我尝试使用在 dgryski/dgoogauth 和 tilaklodha/google-authenticator 中实现的 TOTP 算法。两者都可以很好地处理 16 字节的秘密,但 26 字节的秘密出现错误。
例如对于 16 字节的秘密 VEV2QJEAUN453SR4
:
Time: 2021-12-17 14:31:46
Got: 079119
对于 26 字节的秘密 VEV2QJEAUN453SR4Q4H3AIS4CI
:
Error: "illegal base32 data at input byte 24"
这里是sn-p的代码:
func getHOTPToken(secret string, interval int64) (string, error)
// Converts secret to base32 Encoding
key, err := base32.StdEncoding.DecodeString(secret)
if err != nil
return "", err
// Signing the value using HMAC-SHA1 Algorithm
hash := hmac.New(sha1.New, key)
err = binary.Write(hash, binary.BigEndian, uint64(interval))
if err != nil
return "", err
h := hash.Sum(nil)
// Get 32 bit chunk from hash starting at the offset
offset := h[19] & 0x0f
truncated := binary.BigEndian.Uint32(h[offset : offset+4])
truncated &= 0x7fffffff
code := truncated % 1000000
return fmt.Sprintf("%06d", code), nil
你能告诉我如何处理 26 字节的秘密吗?
【问题讨论】:
请澄清您的具体问题或提供其他详细信息以准确突出您的需求。正如目前所写的那样,很难准确地说出你在问什么。 【参考方案1】:base32 将输入字节的每 5 位编码为 base32 字符,去 base32 使用 RFC 4648 Base 32 字母表(A-Z,2-7)。将字符串解码为字节时,每个 base32 字符输入将被映射到 5 位索引,然后重新组合为字节。
在您的示例“VEV2QJEAUN453SR4Q4H3AIS4CI”中,之前的“VEV2QJEAUN453SR4” 已经是有效输入,它是一个 16 字符输入,而 5 位 * 16 是 80 位,因此可以将其解析为 10 字节输出。现在让我们看看剩下的“Q4H3AIS4CI”, 10 char -> 5 * 10 = 50 bits,前 40 bits 可解码为 5 bytes,但最后 2 char "CI" 领先 2 bit 余数
Q | 4 | H | 3 | A | I | S | 4 | C | I
1 0 0 0 0|1 1 1 0 0|0 0 1 1 1|1 1 0 1 1|0 0 0 0 0|0 1 0 0 0|1 0 0 1 0|1 1 1 0 0|0 0 0 1 0|0 1 0 0 0
1 0 0 0 0 1 1 1|0 0 0 0 1 1 1 1|1 0 1 1 0 0 0 0|0 0 1 0 0 0 1 0|0 1 0 1 1 1 0 0|0 0 0 1 0 0 1 0|0 0
135 | 15 | 176 | 34 | 92 | 18 |
C | I | = | = | = | = | = | = |
0 0 0 1 0|0 1 0 0 0|0 0 0 0 0|0 0 0 0 0|0 0 0 0 0|0 0 0 0 0|0 0 0 0 0|0 0 0 0 0|
0 0 0 1 0 0 1 0|0 0 0 0 0 0 0 0|0 0 0 0 0 0 0 0|0 0 0 0 0 0 0 0|0 0 0 0 0 0 0 0|
18 |
您需要添加 6 个填充 5 % 8 的倍数的余数是:
1 char: 5 % 8 = 5 -> padding 3
2 char: 10 % 8 = 2 -> padding 6 (this case "CI")
3 char: 15 % 8 = 7 -> padding 1
4 char: 20 % 8 = 4 -> padding 4
5 char: 25 % 8 = 1 -> padding 7
6 char: 30 % 8 = 6 -> padding 2
7 char: 35 % 8 = 3 -> padding 5
8 char: 40 % 8 = 0 no padding
我已经修改了你的代码,输入“Q4H3AIS4CI”和 6 填充是可以的
func Base32Test()
// 8 char: 5 * 8 bits -> decodes to 5 bytes
key, err := base32.StdEncoding.DecodeString("Q4H3AIS4")
fmt.Println(key)
if err != nil
fmt.Println("test 1, ", err)
else
fmt.Println("test 1 ok", key)
// 10 char: 5 * 10 bits -> decodes to 5 bytes and remaider (2 bits but the last 10 bits can not be decode)
key, err = base32.StdEncoding.DecodeString("Q4H3AIS4CI")
fmt.Println(key)
if err != nil
fmt.Println("test 2, ", err)
else
fmt.Println("test 2 ok", key)
// padding
key, err = base32.StdEncoding.DecodeString("Q4H3AIS4CI======")
fmt.Println(key)
if err != nil
fmt.Println("test 3, ", err)
else
fmt.Println("test 3 ok", key)
【讨论】:
谢谢!填充很重要——所以我似乎应该添加填充检查,如下所示:if rmd := len(secret) * 5 % 8; rmd != 0 secret += strings.Repeat("=", 8-rmd)
以上是关于如何处理基于时间的一次性密码的 26 字节秘密?的主要内容,如果未能解决你的问题,请参考以下文章