154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
package totp_test
|
|
|
|
import (
|
|
"encoding/base32"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"gateway/internal/model/member/totp"
|
|
)
|
|
|
|
// RFC 6238 Appendix B test vector (HMAC-SHA1, 20-byte ASCII "12345678901234567890").
|
|
// Time 59 → expected 8-digit value 94287082; for 6 digits the truncation is 287082.
|
|
func TestGenerate_RFC6238Vector(t *testing.T) {
|
|
secret := []byte("12345678901234567890")
|
|
code, err := totp.Generate(secret, time.Unix(59, 0), 30*time.Second, 6)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "287082", code)
|
|
|
|
code, err = totp.Generate(secret, time.Unix(1111111109, 0), 30*time.Second, 6)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "081804", code)
|
|
}
|
|
|
|
func TestVerify_WindowAccepts(t *testing.T) {
|
|
secret := []byte("12345678901234567890")
|
|
ts := time.Unix(59, 0)
|
|
code, err := totp.Generate(secret, ts, 30*time.Second, 6)
|
|
require.NoError(t, err)
|
|
|
|
step, ok := totp.Verify(secret, code, ts.Add(15*time.Second), 30*time.Second, 6, 1)
|
|
require.True(t, ok)
|
|
require.Equal(t, totp.TimeStep(ts, 30*time.Second), step)
|
|
}
|
|
|
|
func TestVerify_WindowRejectsBeyond(t *testing.T) {
|
|
secret := []byte("12345678901234567890")
|
|
ts := time.Unix(59, 0)
|
|
code, err := totp.Generate(secret, ts, 30*time.Second, 6)
|
|
require.NoError(t, err)
|
|
|
|
_, ok := totp.Verify(secret, code, ts.Add(2*time.Minute), 30*time.Second, 6, 1)
|
|
require.False(t, ok)
|
|
}
|
|
|
|
func TestVerify_InvalidInputs(t *testing.T) {
|
|
_, ok := totp.Verify(nil, "123456", time.Now(), 30*time.Second, 6, 1)
|
|
require.False(t, ok)
|
|
_, ok = totp.Verify([]byte("k"), "", time.Now(), 30*time.Second, 6, 1)
|
|
require.False(t, ok)
|
|
_, ok = totp.Verify([]byte("k"), "12345", time.Now(), 30*time.Second, 6, 1)
|
|
require.False(t, ok)
|
|
}
|
|
|
|
func TestGenerateSecret(t *testing.T) {
|
|
s1, err := totp.GenerateSecret()
|
|
require.NoError(t, err)
|
|
require.Len(t, s1, totp.SecretBytes)
|
|
|
|
s2, err := totp.GenerateSecret()
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, s1, s2)
|
|
}
|
|
|
|
func TestEncodeDecodeSecret_RoundTrip(t *testing.T) {
|
|
secret, err := totp.GenerateSecret()
|
|
require.NoError(t, err)
|
|
|
|
encoded := totp.EncodeSecret(secret)
|
|
require.NotContains(t, encoded, "=")
|
|
|
|
out, err := totp.DecodeSecret(encoded)
|
|
require.NoError(t, err)
|
|
require.Equal(t, secret, out)
|
|
|
|
padded := encoded
|
|
if mod := len(padded) % 8; mod != 0 {
|
|
padded += strings.Repeat("=", 8-mod)
|
|
}
|
|
require.Equal(t, secret, mustDecodeBase32(t, padded))
|
|
}
|
|
|
|
func mustDecodeBase32(t *testing.T, s string) []byte {
|
|
t.Helper()
|
|
b, err := base32.StdEncoding.DecodeString(s)
|
|
require.NoError(t, err)
|
|
return b
|
|
}
|
|
|
|
func TestDecodeSecret_Invalid(t *testing.T) {
|
|
_, err := totp.DecodeSecret("")
|
|
require.ErrorIs(t, err, totp.ErrInvalidSecret)
|
|
|
|
_, err = totp.DecodeSecret("not!base32!!")
|
|
require.ErrorIs(t, err, totp.ErrInvalidSecret)
|
|
}
|
|
|
|
func TestBuildOtpauthURL(t *testing.T) {
|
|
secret := []byte("12345678901234567890")
|
|
u, err := totp.BuildOtpauthURL(totp.OtpauthURLInput{
|
|
Issuer: "CloudEP",
|
|
Account: "ACME:AMEX-10000000",
|
|
Secret: secret,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
parsed, err := url.Parse(u)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "otpauth", parsed.Scheme)
|
|
require.Equal(t, "totp", parsed.Host)
|
|
require.Contains(t, parsed.Path, "CloudEP:")
|
|
|
|
q := parsed.Query()
|
|
require.Equal(t, totp.EncodeSecret(secret), q.Get("secret"))
|
|
require.Equal(t, "CloudEP", q.Get("issuer"))
|
|
require.Equal(t, "SHA1", q.Get("algorithm"))
|
|
require.Equal(t, "6", q.Get("digits"))
|
|
require.Equal(t, "30", q.Get("period"))
|
|
}
|
|
|
|
func TestBuildOtpauthURL_Validation(t *testing.T) {
|
|
_, err := totp.BuildOtpauthURL(totp.OtpauthURLInput{Issuer: "", Account: "a", Secret: []byte("s")})
|
|
require.ErrorIs(t, err, totp.ErrInvalidIssuer)
|
|
|
|
_, err = totp.BuildOtpauthURL(totp.OtpauthURLInput{Issuer: "x", Account: "", Secret: []byte("s")})
|
|
require.ErrorIs(t, err, totp.ErrInvalidAccount)
|
|
|
|
_, err = totp.BuildOtpauthURL(totp.OtpauthURLInput{Issuer: "x", Account: "a", Secret: nil})
|
|
require.ErrorIs(t, err, totp.ErrInvalidSecret)
|
|
}
|
|
|
|
func TestGenerateBackupCodes(t *testing.T) {
|
|
codes, err := totp.GenerateBackupCodes(10, 12)
|
|
require.NoError(t, err)
|
|
require.Len(t, codes, 10)
|
|
|
|
seen := make(map[string]struct{}, len(codes))
|
|
for _, c := range codes {
|
|
require.Len(t, c, 12)
|
|
seen[c] = struct{}{}
|
|
}
|
|
require.Len(t, seen, len(codes), "backup codes should be unique")
|
|
}
|
|
|
|
func TestTimeStep(t *testing.T) {
|
|
period := 30 * time.Second
|
|
require.Equal(t, uint64(2), totp.TimeStep(time.Unix(60, 0), period))
|
|
require.Equal(t, uint64(2), totp.TimeStep(time.Unix(89, 0), period))
|
|
require.Equal(t, uint64(3), totp.TimeStep(time.Unix(90, 0), period))
|
|
}
|