64 lines
1.4 KiB
Go
64 lines
1.4 KiB
Go
package security
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"encoding/base32"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// GenerateTOTPSecret returns a base32 secret without padding.
|
|
func GenerateTOTPSecret() (string, error) {
|
|
b := make([]byte, 20)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
enc := base32.StdEncoding.WithPadding(base32.NoPadding)
|
|
return enc.EncodeToString(b), nil
|
|
}
|
|
|
|
// VerifyTOTP verifies a 6-digit token with ±1 step skew (30s step).
|
|
func VerifyTOTP(secretBase32, code string, now time.Time) bool {
|
|
code = strings.ReplaceAll(code, " ", "")
|
|
if len(code) != 6 {
|
|
return false
|
|
}
|
|
sec, err := decodeBase32NoPad(secretBase32)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
t := now.Unix() / 30
|
|
for _, drift := range []int64{-1, 0, 1} {
|
|
if hotp(sec, uint64(t+drift)) == code {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hotp(secret []byte, counter uint64) string {
|
|
var buf [8]byte
|
|
binary.BigEndian.PutUint64(buf[:], counter)
|
|
mac := hmac.New(sha1.New, secret)
|
|
mac.Write(buf[:])
|
|
sum := mac.Sum(nil)
|
|
|
|
off := sum[len(sum)-1] & 0x0f
|
|
bin := (int(sum[off])&0x7f)<<24 |
|
|
(int(sum[off+1])&0xff)<<16 |
|
|
(int(sum[off+2])&0xff)<<8 |
|
|
(int(sum[off+3]) & 0xff)
|
|
otp := bin % 1000000
|
|
return fmt.Sprintf("%06d", otp)
|
|
}
|
|
|
|
func decodeBase32NoPad(s string) ([]byte, error) {
|
|
s = strings.ToUpper(strings.ReplaceAll(s, " ", ""))
|
|
enc := base32.StdEncoding.WithPadding(base32.NoPadding)
|
|
return enc.DecodeString(s)
|
|
}
|