package totpext import ( "crypto/hmac" "crypto/rand" "crypto/sha1" "encoding/base32" "encoding/binary" "fmt" "hash" "net/url" "strconv" "time" ) // https://datatracker.ietf.org/doc/html/rfc6238 // https://datatracker.ietf.org/doc/html/rfc4226 // https://datatracker.ietf.org/doc/html/rfc2104 // https://en.wikipedia.org/wiki/Universal_2nd_Factor // https://en.wikipedia.org/wiki/HMAC-based_one-time_password // https://en.wikipedia.org/wiki/HMAC func TOTP(key []byte) string { t := time.Now().Unix() / 30 return generateTOTP(sha1.New, key, t, 6) } func Validate(key []byte, totp string) bool { t := time.Now().Unix() / 30 if generateTOTP(sha1.New, key, t, 6) == totp { return true } if generateTOTP(sha1.New, key, t-1, 6) == totp { return true } if generateTOTP(sha1.New, key, t+1, 6) == totp { return true } return false } func GenerateSecret() ([]byte, error) { secret := make([]byte, 20) _, err := rand.Read(secret) if err != nil { return nil, err } return secret, nil } func generateTOTP(algo func() hash.Hash, secret []byte, time int64, returnDigits int) string { msg := make([]byte, 8) binary.BigEndian.PutUint64(msg, uint64(time)) mac := hmac.New(algo, secret) mac.Write(msg) hmacResult := mac.Sum(nil) offsetBits := hmacResult[len(hmacResult)-1] & 0x0F p := hmacResult[offsetBits : offsetBits+4] truncated := binary.BigEndian.Uint32(p) & 0x7FFFFFFF // Last 31 bits val := strconv.Itoa(int(truncated)) for len(val) < returnDigits { val = "0" + val } val = val[len(val)-returnDigits:] return val } func GenerateOTPAuth(ccn string, key []byte, accountmail string, issuer string) string { return fmt.Sprintf("otpauth://totp/%v:%v?secret=%v&issuer=%v&algorithm=%v&period=%v&digits=%v", ccn, url.QueryEscape(accountmail), base32.StdEncoding.EncodeToString(key), issuer, "SHA1", "30", "6") }