git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/otp/totp/totp_test.go (about) 1 /** 2 * Copyright 2014 Paul Querna 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package totp 19 20 import ( 21 "git.sr.ht/~pingoo/stdx/otp" 22 "github.com/stretchr/testify/assert" 23 "github.com/stretchr/testify/require" 24 25 "encoding/base32" 26 "testing" 27 "time" 28 ) 29 30 type tc struct { 31 TS int64 32 TOTP string 33 Mode otp.Algorithm 34 Secret string 35 } 36 37 var ( 38 secSha1 = base32.StdEncoding.EncodeToString([]byte("12345678901234567890")) 39 secSha256 = base32.StdEncoding.EncodeToString([]byte("12345678901234567890123456789012")) 40 secSha512 = base32.StdEncoding.EncodeToString([]byte("1234567890123456789012345678901234567890123456789012345678901234")) 41 42 rfcMatrixTCs = []tc{ 43 {59, "94287082", otp.AlgorithmSHA1, secSha1}, 44 {59, "46119246", otp.AlgorithmSHA256, secSha256}, 45 {59, "90693936", otp.AlgorithmSHA512, secSha512}, 46 {1111111109, "07081804", otp.AlgorithmSHA1, secSha1}, 47 {1111111109, "68084774", otp.AlgorithmSHA256, secSha256}, 48 {1111111109, "25091201", otp.AlgorithmSHA512, secSha512}, 49 {1111111111, "14050471", otp.AlgorithmSHA1, secSha1}, 50 {1111111111, "67062674", otp.AlgorithmSHA256, secSha256}, 51 {1111111111, "99943326", otp.AlgorithmSHA512, secSha512}, 52 {1234567890, "89005924", otp.AlgorithmSHA1, secSha1}, 53 {1234567890, "91819424", otp.AlgorithmSHA256, secSha256}, 54 {1234567890, "93441116", otp.AlgorithmSHA512, secSha512}, 55 {2000000000, "69279037", otp.AlgorithmSHA1, secSha1}, 56 {2000000000, "90698825", otp.AlgorithmSHA256, secSha256}, 57 {2000000000, "38618901", otp.AlgorithmSHA512, secSha512}, 58 {20000000000, "65353130", otp.AlgorithmSHA1, secSha1}, 59 {20000000000, "77737706", otp.AlgorithmSHA256, secSha256}, 60 {20000000000, "47863826", otp.AlgorithmSHA512, secSha512}, 61 } 62 ) 63 64 // Test vectors from http://tools.ietf.org/html/rfc6238#appendix-B 65 // NOTE -- the test vectors are documented as having the SAME 66 // secret -- this is WRONG -- they have a variable secret 67 // depending upon the hmac algorithm: 68 // 69 // http://www.rfc-editor.org/errata_search.php?rfc=6238 70 // 71 // this only took a few hours of head/desk interaction to figure out. 72 func TestValidateRFCMatrix(t *testing.T) { 73 for _, tx := range rfcMatrixTCs { 74 valid, err := ValidateCustom(tx.TOTP, tx.Secret, time.Unix(tx.TS, 0).UTC(), 75 ValidateOpts{ 76 Digits: otp.DigitsEight, 77 Algorithm: tx.Mode, 78 }) 79 require.NoError(t, err, 80 "unexpected error totp=%s mode=%v ts=%v", tx.TOTP, tx.Mode, tx.TS) 81 require.True(t, valid, 82 "unexpected totp failure totp=%s mode=%v ts=%v", tx.TOTP, tx.Mode, tx.TS) 83 } 84 } 85 86 func TestGenerateRFCTCs(t *testing.T) { 87 for _, tx := range rfcMatrixTCs { 88 passcode, err := GenerateCodeCustom(tx.Secret, time.Unix(tx.TS, 0).UTC(), 89 ValidateOpts{ 90 Digits: otp.DigitsEight, 91 Algorithm: tx.Mode, 92 }) 93 assert.Nil(t, err) 94 assert.Equal(t, tx.TOTP, passcode) 95 } 96 } 97 98 func TestValidateSkew(t *testing.T) { 99 secSha1 := base32.StdEncoding.EncodeToString([]byte("12345678901234567890")) 100 101 tests := []tc{ 102 {29, "94287082", otp.AlgorithmSHA1, secSha1}, 103 {59, "94287082", otp.AlgorithmSHA1, secSha1}, 104 {61, "94287082", otp.AlgorithmSHA1, secSha1}, 105 } 106 107 for _, tx := range tests { 108 valid, err := ValidateCustom(tx.TOTP, tx.Secret, time.Unix(tx.TS, 0).UTC(), 109 ValidateOpts{ 110 Digits: otp.DigitsEight, 111 Algorithm: tx.Mode, 112 Skew: 1, 113 }) 114 require.NoError(t, err, 115 "unexpected error totp=%s mode=%v ts=%v", tx.TOTP, tx.Mode, tx.TS) 116 require.True(t, valid, 117 "unexpected totp failure totp=%s mode=%v ts=%v", tx.TOTP, tx.Mode, tx.TS) 118 } 119 } 120 121 func TestGenerate(t *testing.T) { 122 k, err := Generate(GenerateOpts{ 123 Issuer: "SnakeOil", 124 AccountName: "alice@example.com", 125 }) 126 require.NoError(t, err, "generate basic TOTP") 127 require.Equal(t, "SnakeOil", k.Issuer(), "Extracting Issuer") 128 require.Equal(t, "alice@example.com", k.AccountName(), "Extracting Account Name") 129 require.Equal(t, 32, len(k.Secret()), "Secret is 32 bytes long as base32.") 130 131 k, err = Generate(GenerateOpts{ 132 Issuer: "SnakeOil", 133 AccountName: "alice@example.com", 134 SecretSize: 20, 135 }) 136 require.NoError(t, err, "generate larger TOTP") 137 require.Equal(t, 32, len(k.Secret()), "Secret is 32 bytes long as base32.") 138 139 k, err = Generate(GenerateOpts{ 140 Issuer: "SnakeOil", 141 AccountName: "alice@example.com", 142 SecretSize: 13, // anything that is not divisable by 5, really 143 }) 144 require.NoError(t, err, "Secret size is valid when length not divisable by 5.") 145 require.NotContains(t, k.Secret(), "=", "Secret has no escaped characters.") 146 147 k, err = Generate(GenerateOpts{ 148 Issuer: "SnakeOil", 149 AccountName: "alice@example.com", 150 Secret: []byte("helloworld"), 151 }) 152 require.NoError(t, err, "Secret generation failed") 153 sec, err := b32NoPadding.DecodeString(k.Secret()) 154 require.NoError(t, err, "Secret wa not valid base32") 155 require.Equal(t, sec, []byte("helloworld"), "Specified Secret was not kept") 156 } 157 158 func TestGoogleLowerCaseSecret(t *testing.T) { 159 w, err := otp.NewKeyFromURL(`otpauth://totp/Google%3Afoo%40example.com?secret=qlt6vmy6svfx4bt4rpmisaiyol6hihca&issuer=Google`) 160 require.NoError(t, err) 161 sec := w.Secret() 162 require.Equal(t, "qlt6vmy6svfx4bt4rpmisaiyol6hihca", sec) 163 164 n := time.Now().UTC() 165 code, err := GenerateCode(w.Secret(), n) 166 require.NoError(t, err) 167 168 valid := Validate(code, w.Secret()) 169 require.True(t, valid) 170 }