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