1
17
18 package totp
19
20 import (
21 "github.com/pquerna/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
65
66
67
68
69
70
71
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,
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 }
171
View as plain text