// Copyright 2024-2025 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package totp import ( "crypto/hmac" "crypto/sha1" //nolint:gosec // This is an implementation of an RFC that used SHA-1 "encoding/base32" "encoding/binary" "fmt" "math" "strings" "testing" "time" "github.com/stretchr/testify/require" ) // This code is borrowed from // https://github.com/yitsushi/totp-cli/blob/b26f5673ae2e5cc682fc1f5ed771cb08a6403283/internal/security/otp.go // and // https://github.com/yitsushi/totp-cli/blob/b26f5673ae2e5cc682fc1f5ed771cb08a6403283/internal/security/error.go // which is MIT licensed. The MIT license allows copying. // We are choosing to copying rather than take on a whole new project dependency just for a small test helper. const ( mask1 = 0xf mask2 = 0x7f mask3 = 0xff timeSplitInSeconds = 30 shift24 = 24 shift16 = 16 shift8 = 8 sumByteLength = 8 ) // OTPError is an error describing an error during generation. type OTPError struct { Message string } func (e OTPError) Error() string { return "otp error: " + e.Message } // GenerateOTPCode generates a 6 digit TOTP from the secret Token. func GenerateOTPCode(t *testing.T, token string, when time.Time) (string, int64) { t.Helper() require.NotEmpty(t, token) timer := uint64(math.Floor(float64(when.Unix()) / float64(timeSplitInSeconds))) remainingTime := timeSplitInSeconds - when.Unix()%timeSplitInSeconds // Remove spaces, some providers are giving us in a readable format, // so they add spaces in there. If it's not removed while pasting in, // remove it now. token = strings.ReplaceAll(token, " ", "") // It should be uppercase always token = strings.ToUpper(token) secretBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(token) require.NoError(t, err) length := 6 buf := make([]byte, sumByteLength) mac := hmac.New(sha1.New, secretBytes) binary.BigEndian.PutUint64(buf, timer) _, _ = mac.Write(buf) sum := mac.Sum(nil) // http://tools.ietf.org/html/rfc4226#section-5.4 offset := sum[len(sum)-1] & mask1 value := int64(((int(sum[offset]) & mask2) << shift24) | ((int(sum[offset+1] & mask3)) << shift16) | ((int(sum[offset+2] & mask3)) << shift8) | (int(sum[offset+3]) & mask3)) modulo := int32(value % int64(math.Pow10(length))) //nolint:gosec // the resulting number must be less than 10^6 format := fmt.Sprintf("%%0%dd", length) return fmt.Sprintf(format, modulo), remainingTime }