2022-07-03 21:46:12 +02:00

183 lines
5.2 KiB
Go

// Copyright (C) 2019 Michael J. Fromberger. All Rights Reserved.
// Package otp generates single use authenticator codes using the HOTP or TOTP
// algorithms specified in RFC 4226 and RFC 6238 respectively.
//
// See https://tools.ietf.org/html/rfc4226, https://tools.ietf.org/html/rfc6238
package otp
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base32"
"encoding/binary"
"fmt"
"hash"
"strconv"
"strings"
"time"
)
// DefaultTOTP generates a TOTP for the current time step using the default
// settings (compatible with Google Authenticator) based on the given key.
// An error is reported if the key is invalid.
func DefaultTOTP(key string) (string, error) {
var std Config
if err := std.ParseKey(key); err != nil {
return "", err
}
return std.TOTP(), nil
}
// DefaultHOTP generates an HTOP for the specified counter using the default
// settings (compatible with Google Authenticator) based on the given key.
// An error is reported if the key is invalid.
func DefaultHOTP(key string, counter uint64) (string, error) {
var std Config
if err := std.ParseKey(key); err != nil {
return "", err
}
return std.HOTP(counter), nil
}
// TimeWindow returns a time step generator that yields the number of n-second
// intervals elapsed at the current wallclock time since the Unix epoch.
func TimeWindow(n int) func() uint64 {
return func() uint64 { return uint64(time.Now().Unix()) / uint64(n) }
}
var timeWindow30 = TimeWindow(30) // default 30-second window
// Config holds the settings that control generation of authentication codes.
// The only required field is Key. The other fields may be omitted, and will
// use default values compatible with the Google authenticator.
type Config struct {
Key string // shared secret between server and user (required)
Hash func() hash.Hash // hash constructor (default is sha1.New)
TimeStep func() uint64 // TOTP time step (default is TimeWindow(30))
Counter uint64 // HOTP counter value
Digits int // number of OTP digits (default 6)
// If set, this function is called with the truncated counter hash to format
// a code of the specified width. By default, the code is formatted as
// decimal digits (0..9).
//
// If Format returns a string of the wrong length, code generation panics.
Format func(v uint64, width int) string
}
// ParseKey parses a base32 key using the top-level ParseKey function, and
// stores the result in c.
func (c *Config) ParseKey(s string) error {
dec, err := ParseKey(s)
if err != nil {
return err
}
c.Key = string(dec)
return nil
}
// ParseKey parses a key encoded as base32, the format used by common
// two-factor authentication setup tools. Whitespace is ignored, case is
// normalized, and padding is added if required.
func ParseKey(s string) ([]byte, error) {
clean := strings.ToUpper(strings.Join(strings.Fields(s), ""))
if n := len(clean) % 8; n != 0 {
clean += "========"[:8-n]
}
return base32.StdEncoding.DecodeString(clean)
}
// HOTP returns the HOTP code for the specified counter value.
func (c Config) HOTP(counter uint64) string {
nd := c.digits()
code := c.format(truncate(c.hmac(counter)), nd)
if len(code) != nd {
panic(fmt.Sprintf("invalid code length: got %d, want %d", len(code), nd))
}
return code
}
// Next increments the counter and returns the HOTP corresponding to its new value.
func (c *Config) Next() string { c.Counter++; return c.HOTP(c.Counter) }
// TOTP returns the TOTP code for the current time step. If the current time
// step value is t, this is equivalent to c.HOTP(t).
func (c Config) TOTP() string {
return c.HOTP(c.timeStepWindow())
}
func (c Config) newHash() func() hash.Hash {
if c.Hash != nil {
return c.Hash
}
return sha1.New
}
func (c Config) digits() int {
if c.Digits <= 0 {
return 6
}
return c.Digits
}
func (c Config) timeStepWindow() uint64 {
if c.TimeStep != nil {
return c.TimeStep()
}
return timeWindow30()
}
func (c Config) hmac(counter uint64) []byte {
var ctr [8]byte
binary.BigEndian.PutUint64(ctr[:], uint64(counter))
h := hmac.New(c.newHash(), []byte(c.Key))
h.Write(ctr[:])
return h.Sum(nil)
}
func (c Config) format(v uint64, nd int) string {
if c.Format != nil {
return c.Format(v, nd)
}
return format(v, nd)
}
func truncate(digest []byte) uint64 {
offset := digest[len(digest)-1] & 0x0f
code := (uint64(digest[offset]&0x7f) << 24) |
(uint64(digest[offset+1]) << 16) |
(uint64(digest[offset+2]) << 8) |
(uint64(digest[offset+3]) << 0)
return code
}
func format(code uint64, width int) string {
const padding = "00000000000000000000"
s := strconv.FormatUint(code, 10)
if len(s) < width {
s = padding[:width-len(s)] + s // left-pad with zeros
}
return s[len(s)-width:]
}
// FormatAlphabet constructs a formatting function that maps code digits to the
// coresponding letters of the given alphabet string. Code digits are expanded
// from most to least significant.
func FormatAlphabet(alphabet string) func(uint64, int) string {
if alphabet == "" {
panic("empty formatting alphabet")
}
return func(code uint64, width int) string {
w := uint64(len(alphabet))
out := make([]byte, width)
for i := width - 1; i >= 0; i-- {
out[i] = alphabet[int(code%w)]
code /= w
}
return string(out)
}
}