mirror of
https://github.com/ghostersk/gowebmail.git
synced 2026-04-17 16:46:01 +01:00
first commit
This commit is contained in:
100
internal/mfa/totp.go
Normal file
100
internal/mfa/totp.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Package mfa provides TOTP-based two-factor authentication (RFC 6238).
|
||||
// Compatible with Google Authenticator, Authy, and any standard TOTP app.
|
||||
// No external dependencies — uses only the Go standard library.
|
||||
package mfa
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/base32"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
totpDigits = 6
|
||||
totpPeriod = 30 // seconds
|
||||
totpWindow = 1 // accept ±1 period to allow for clock skew
|
||||
)
|
||||
|
||||
// GenerateSecret creates a new random 20-byte (160-bit) TOTP secret,
|
||||
// returned as a base32-encoded string (the standard format for authenticator apps).
|
||||
func GenerateSecret() (string, error) {
|
||||
secret := make([]byte, 20)
|
||||
if _, err := rand.Read(secret); err != nil {
|
||||
return "", fmt.Errorf("generate secret: %w", err)
|
||||
}
|
||||
return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(secret), nil
|
||||
}
|
||||
|
||||
// OTPAuthURL builds an otpauth:// URI for QR code generation.
|
||||
// issuer is the application name (e.g. "GoMail"), accountName is the user's email.
|
||||
func OTPAuthURL(issuer, accountName, secret string) string {
|
||||
v := url.Values{}
|
||||
v.Set("secret", secret)
|
||||
v.Set("issuer", issuer)
|
||||
v.Set("algorithm", "SHA1")
|
||||
v.Set("digits", fmt.Sprintf("%d", totpDigits))
|
||||
v.Set("period", fmt.Sprintf("%d", totpPeriod))
|
||||
|
||||
label := url.PathEscape(issuer + ":" + accountName)
|
||||
return fmt.Sprintf("otpauth://totp/%s?%s", label, v.Encode())
|
||||
}
|
||||
|
||||
// QRCodeURL returns a Google Charts URL that renders the otpauth URI as a QR code.
|
||||
// In production you'd generate this server-side; this is convenient for self-hosted use.
|
||||
func QRCodeURL(issuer, accountName, secret string) string {
|
||||
otpURL := OTPAuthURL(issuer, accountName, secret)
|
||||
return fmt.Sprintf(
|
||||
"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=%s",
|
||||
url.QueryEscape(otpURL),
|
||||
)
|
||||
}
|
||||
|
||||
// Validate checks whether code is a valid TOTP code for secret at the current time.
|
||||
// It accepts codes from [now-window*period, now+window*period] to handle clock skew.
|
||||
func Validate(secret, code string) bool {
|
||||
code = strings.TrimSpace(code)
|
||||
if len(code) != totpDigits {
|
||||
return false
|
||||
}
|
||||
keyBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(
|
||||
strings.ToUpper(secret),
|
||||
)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
counter := now / totpPeriod
|
||||
for delta := int64(-totpWindow); delta <= int64(totpWindow); delta++ {
|
||||
if totp(keyBytes, counter+delta) == code {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// totp computes a 6-digit TOTP for the given key and counter (RFC 6238 / HOTP RFC 4226).
|
||||
func totp(key []byte, counter int64) string {
|
||||
msg := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(msg, uint64(counter))
|
||||
|
||||
mac := hmac.New(sha1.New, key)
|
||||
mac.Write(msg)
|
||||
h := mac.Sum(nil)
|
||||
|
||||
// Dynamic truncation
|
||||
offset := h[len(h)-1] & 0x0f
|
||||
code := (int64(h[offset]&0x7f) << 24) |
|
||||
(int64(h[offset+1]) << 16) |
|
||||
(int64(h[offset+2]) << 8) |
|
||||
int64(h[offset+3])
|
||||
|
||||
otp := code % int64(math.Pow10(totpDigits))
|
||||
return fmt.Sprintf("%0*d", totpDigits, otp)
|
||||
}
|
||||
Reference in New Issue
Block a user