goauthenticationone-time-passwordtotp

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


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?


Solution

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