goext/totpext/totp.go

90 lines
1.8 KiB
Go

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")
}