git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/otp/hotp/hotp_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 hotp 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 ) 28 29 type tc struct { 30 Counter uint64 31 TOTP string 32 Mode otp.Algorithm 33 Secret string 34 } 35 36 var ( 37 secSha1 = base32.StdEncoding.EncodeToString([]byte("12345678901234567890")) 38 39 rfcMatrixTCs = []tc{ 40 {0, "755224", otp.AlgorithmSHA1, secSha1}, 41 {1, "287082", otp.AlgorithmSHA1, secSha1}, 42 {2, "359152", otp.AlgorithmSHA1, secSha1}, 43 {3, "969429", otp.AlgorithmSHA1, secSha1}, 44 {4, "338314", otp.AlgorithmSHA1, secSha1}, 45 {5, "254676", otp.AlgorithmSHA1, secSha1}, 46 {6, "287922", otp.AlgorithmSHA1, secSha1}, 47 {7, "162583", otp.AlgorithmSHA1, secSha1}, 48 {8, "399871", otp.AlgorithmSHA1, secSha1}, 49 {9, "520489", otp.AlgorithmSHA1, secSha1}, 50 } 51 ) 52 53 // Test values from http://tools.ietf.org/html/rfc4226#appendix-D 54 func TestValidateRFCMatrix(t *testing.T) { 55 56 for _, tx := range rfcMatrixTCs { 57 valid, err := ValidateCustom(tx.TOTP, tx.Counter, tx.Secret, 58 ValidateOpts{ 59 Digits: otp.DigitsSix, 60 Algorithm: tx.Mode, 61 }) 62 require.NoError(t, err, 63 "unexpected error totp=%s mode=%v counter=%v", tx.TOTP, tx.Mode, tx.Counter) 64 require.True(t, valid, 65 "unexpected totp failure totp=%s mode=%v counter=%v", tx.TOTP, tx.Mode, tx.Counter) 66 } 67 } 68 69 func TestGenerateRFCMatrix(t *testing.T) { 70 for _, tx := range rfcMatrixTCs { 71 passcode, err := GenerateCodeCustom(tx.Secret, tx.Counter, 72 ValidateOpts{ 73 Digits: otp.DigitsSix, 74 Algorithm: tx.Mode, 75 }) 76 assert.Nil(t, err) 77 assert.Equal(t, tx.TOTP, passcode) 78 } 79 } 80 81 func TestValidateInvalid(t *testing.T) { 82 secSha1 := base32.StdEncoding.EncodeToString([]byte("12345678901234567890")) 83 84 valid, err := ValidateCustom("foo", 11, secSha1, 85 ValidateOpts{ 86 Digits: otp.DigitsSix, 87 Algorithm: otp.AlgorithmSHA1, 88 }) 89 require.Equal(t, otp.ErrValidateInputInvalidLength, err, "Expected Invalid length error.") 90 require.Equal(t, false, valid, "Valid should be false when we have an error.") 91 92 valid, err = ValidateCustom("foo", 11, secSha1, 93 ValidateOpts{ 94 Digits: otp.DigitsEight, 95 Algorithm: otp.AlgorithmSHA1, 96 }) 97 require.Equal(t, otp.ErrValidateInputInvalidLength, err, "Expected Invalid length error.") 98 require.Equal(t, false, valid, "Valid should be false when we have an error.") 99 100 valid, err = ValidateCustom("000000", 11, secSha1, 101 ValidateOpts{ 102 Digits: otp.DigitsSix, 103 Algorithm: otp.AlgorithmSHA1, 104 }) 105 require.NoError(t, err, "Expected no error.") 106 require.Equal(t, false, valid, "Valid should be false.") 107 108 valid = Validate("000000", 11, secSha1) 109 require.Equal(t, false, valid, "Valid should be false.") 110 } 111 112 // This tests for issue #10 - secrets without padding 113 func TestValidatePadding(t *testing.T) { 114 valid, err := ValidateCustom("831097", 0, "JBSWY3DPEHPK3PX", 115 ValidateOpts{ 116 Digits: otp.DigitsSix, 117 Algorithm: otp.AlgorithmSHA1, 118 }) 119 require.NoError(t, err, "Expected no error.") 120 require.Equal(t, true, valid, "Valid should be true.") 121 } 122 123 func TestValidateLowerCaseSecret(t *testing.T) { 124 valid, err := ValidateCustom("831097", 0, "jbswy3dpehpk3px", 125 ValidateOpts{ 126 Digits: otp.DigitsSix, 127 Algorithm: otp.AlgorithmSHA1, 128 }) 129 require.NoError(t, err, "Expected no error.") 130 require.Equal(t, true, valid, "Valid should be true.") 131 } 132 133 func TestGenerate(t *testing.T) { 134 k, err := Generate(GenerateOpts{ 135 Issuer: "SnakeOil", 136 AccountName: "alice@example.com", 137 }) 138 require.NoError(t, err, "generate basic TOTP") 139 require.Equal(t, "SnakeOil", k.Issuer(), "Extracting Issuer") 140 require.Equal(t, "alice@example.com", k.AccountName(), "Extracting Account Name") 141 require.Equal(t, 16, len(k.Secret()), "Secret is 16 bytes long as base32.") 142 143 k, err = Generate(GenerateOpts{ 144 Issuer: "SnakeOil", 145 AccountName: "alice@example.com", 146 SecretSize: 20, 147 }) 148 require.NoError(t, err, "generate larger TOTP") 149 require.Equal(t, 32, len(k.Secret()), "Secret is 32 bytes long as base32.") 150 151 k, err = Generate(GenerateOpts{ 152 Issuer: "", 153 AccountName: "alice@example.com", 154 }) 155 require.Equal(t, otp.ErrGenerateMissingIssuer, err, "generate missing issuer") 156 require.Nil(t, k, "key should be nil on error.") 157 158 k, err = Generate(GenerateOpts{ 159 Issuer: "Foobar, Inc", 160 AccountName: "", 161 }) 162 require.Equal(t, otp.ErrGenerateMissingAccountName, err, "generate missing account name.") 163 require.Nil(t, k, "key should be nil on error.") 164 165 k, err = Generate(GenerateOpts{ 166 Issuer: "SnakeOil", 167 AccountName: "alice@example.com", 168 SecretSize: 17, // anything that is not divisable by 5, really 169 }) 170 require.NoError(t, err, "Secret size is valid when length not divisable by 5.") 171 require.NotContains(t, k.Secret(), "=", "Secret has no escaped characters.") 172 173 k, err = Generate(GenerateOpts{ 174 Issuer: "SnakeOil", 175 AccountName: "alice@example.com", 176 Secret: []byte("helloworld"), 177 }) 178 require.NoError(t, err, "Secret generation failed") 179 sec, err := b32NoPadding.DecodeString(k.Secret()) 180 require.NoError(t, err, "Secret wa not valid base32") 181 require.Equal(t, sec, []byte("helloworld"), "Specified Secret was not kept") 182 }