如何处理26字节的时间基一次性密码(TOTP)的秘密?

huangapple go评论73阅读模式
英文:

How to Handle 26-Byte Secret for Time-based One Time Password?

问题

26字节的密钥是使用Base32编码的字符串,通常以小写字母和空格的形式出现。要处理这样的密钥,你可以使用以下步骤:

  1. 将密钥转换为Base32编码。
  2. 使用HMAC-SHA1算法对值进行签名。
  3. 从哈希中获取32位的块,从偏移量开始。
  4. 对截断的值进行处理,确保它是正整数。
  5. 将截断的值取模1000000,得到一个6位数的验证码。

你可以使用下面的代码片段来处理26字节的密钥:

import (
	"encoding/base32"
	"crypto/hmac"
	"crypto/sha1"
	"encoding/binary"
	"fmt"
)

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
}

func main() {
	secret := "VEV2QJEAUN453SR4Q4H3AIS4CI"
	interval := int64(1234567890) // Replace with your desired interval
	token, err := getHOTPToken(secret, interval)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("Token:", token)
}

你可以将密钥和时间间隔替换为你自己的值,然后调用getHOTPToken函数来获取验证码。

英文:

Secret of Time-based One Time Password are usually 16-byte base32 encoded string. e.g. GitHub 2FA.

But for some scenario, it has 26 bytes long. e.g. Tutanota OTP. Often in lower case with whitespaces, like: vev2 qjea un45 3sr4 q4h3 ais4 ci

I tried with the TOTP algorithm implemented in dgryski/dgoogauth and tilaklodha/google-authenticator. Both can handle 16-byte secret well, but got error for 26-byte secret.

e.g. for 16-byte secret VEV2QJEAUN453SR4:

Time: 2021-12-17 14:31:46
Got: 079119

for 26-byte secret VEV2QJEAUN453SR4Q4H3AIS4CI:

Error: "illegal base32 data at input byte 24"

Here's the code snippet:

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
}

Can you please tell me how to handle 26-byte secret?

答案1

得分: 2

每5位输入字节,base32编码为一个base32字符,go base32使用RFC 4648 Base 32字母表(A-Z,2-7)。解码字符串为字节时,每个base32字符输入将映射到一个5位索引,然后重新组合为字节。

在你的例子中,"VEV2QJEAUN453SR4Q4H3AIS4CI"之前的"VEV2QJEAUN453SR4"已经是有效的输入,它是一个16个字符的输入,5位 * 16是80位,因此可以解析为10个字节的输出。现在让我们看看剩下的"Q4H3AIS4CI",10个字符-> 5 * 10 = 50位,前面的40位可以解码为5个字节,但最后的2个字符"CI"导致2位余数。

你需要添加6个填充字符。
5的倍数的余数位% 8是:

                          其位数每八个字符可被整除
按位操作                 按字节操作        填充字符
1个字符:5  % 8 = 5位   |  1 % 8 (字符) = 1 -> 7个字符
2个字符:10 % 8 = 2位  |  2 % 8 (字符) = 2 -> 6个字符(这种情况下是"CI")
3个字符:15 % 8 = 7位  |  3 % 8 (字符) = 3 -> 5个字符
4个字符:20 % 8 = 4位  |  4 % 8 (字符) = 4 -> 4个字符
5个字符:25 % 8 = 1位  |  5 % 8 (字符) = 5 -> 3个字符
6个字符:30 % 8 = 6位  |  6 % 8 (字符) = 6 -> 2个字符
7个字符:35 % 8 = 3位  |  7 % 8 (字符) = 7 -> 2个字符
8个字符:40 % 8 = 0位  |  8 % 8 (字符) = 8 -> 0个字符

我已经修改了你的代码,输入"Q4H3AIS4CI"并添加了6个填充字符是可以的。

func Base32Test() {
	// 8个字符:5 * 8位 -> 解码为5个字节
	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个字符:5 * 10位 -> 解码为5个字节和余数(2位,但最后的10位无法解码)
	key, err = base32.StdEncoding.DecodeString("Q4H3AIS4CI")
	fmt.Println(key)
	if err != nil {
		fmt.Println("test 2, ", err)
	} else {
		fmt.Println("test 2 ok", key)
	}
	// 填充
	key, err = base32.StdEncoding.DecodeString("Q4H3AIS4CI======")
	fmt.Println(key)
	if err != nil {
		fmt.Println("test 3, ", err)
	} else {
		fmt.Println("test 3 ok", key)
	}
}
英文:

A base32 encodes every 5 bits of input bytes into base32 character, go base32 use
The RFC 4648 Base 32 alphabet (A-Z, 2-7). When decode a string to bytes, each base32
character input will be mapped to a 5 bit index then recompose to bytes.

In your example "VEV2QJEAUN453SR4Q4H3AIS4CI", the previous "VEV2QJEAUN453SR4"
was already valid input, it is a 16 char input, and 5 bit * 16 is 80 bit so it can be resolved into 10 bytes output. Now let us just look at the rest "Q4H3AIS4CI",
10 char -> 5 * 10 = 50 bits, the previous 40 bits can be decode to 5 bytes, but the last 2 char "CI" leads 2 bit remainder

    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        |

You need to add 6 paddings
the remainder bit of multiples of 5 % 8 is:

                          Its bits are divisible every eight chars
bitwise opinion           byte opinion        padding chars
1 char: 5  % 8 = 5 bit  |  1 % 8 (char) = 1 -> 7 char
2 char: 10 % 8 = 2 bit  |  2 % 8 (char) = 2 -> 6 char (this case "CI")
3 char: 15 % 8 = 7 bit  |  3 % 8 (char) = 3 -> 5 char
4 char: 20 % 8 = 4 bit  |  4 % 8 (char) = 4 -> 4 char
5 char: 25 % 8 = 1 bit  |  5 % 8 (char) = 5 -> 3 char
6 char: 30 % 8 = 6 bit  |  6 % 8 (char) = 6 -> 2 char
7 char: 35 % 8 = 3 bit  |  7 % 8 (char) = 7 -> 2 char
8 char: 40 % 8 = 0 bit  |  8 % 8 (char) = 8 -> 0 char


I have modified your code, the inputs "Q4H3AIS4CI" with 6 padding is ok

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)
	}
}

huangapple
  • 本文由 发表于 2021年12月17日 14:36:25
  • 转载请务必保留本文链接:https://go.coder-hub.com/70389299.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定