125 lines
2.7 KiB
Go
125 lines
2.7 KiB
Go
package otp
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"crypto/subtle"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
Scheme = "otpauth"
|
|
DefaultLength = 6
|
|
DefaultPeriod = uint64(30)
|
|
DefaultIssuer = "justinjudd.org"
|
|
DefaultAllowedSkew = 1
|
|
DefaultLabel = "test"
|
|
DefaultCounter = uint64(0)
|
|
)
|
|
|
|
// Error is a custom error type for this package
|
|
type Error string
|
|
|
|
// Error satisfies the error interface
|
|
func (e Error) Error() string {
|
|
return "otp: " + string(e)
|
|
}
|
|
|
|
// Type is used to differentiate different OTP types - HOTP and TOTP
|
|
type Type int
|
|
|
|
const (
|
|
UnknownType Type = iota
|
|
HOTP
|
|
TOTP
|
|
)
|
|
|
|
// String satisfies the Stringer interface for Type
|
|
func (t Type) String() string {
|
|
switch t {
|
|
case HOTP:
|
|
return "hotp"
|
|
case TOTP:
|
|
return "totp"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
// NewType creates a Type object from its string representation
|
|
func NewType(t string) Type {
|
|
switch strings.ToLower(t) {
|
|
case "hotp":
|
|
return HOTP
|
|
case "totp":
|
|
return TOTP
|
|
default:
|
|
return UnknownType
|
|
}
|
|
}
|
|
|
|
// getOTP creates a Int representation of a OTP
|
|
func getOTP(secret []byte, counter uint64, length uint) *big.Int {
|
|
mac := hmac.New(sha1.New, secret)
|
|
|
|
b := new(big.Int)
|
|
b.SetUint64(counter)
|
|
|
|
buf := new(bytes.Buffer)
|
|
binary.Write(buf, binary.BigEndian, b.Uint64())
|
|
mac.Write(buf.Bytes())
|
|
s := mac.Sum(nil)
|
|
i := dynamicTruncation(s)
|
|
t := new(big.Int)
|
|
t.Exp(big.NewInt(10), big.NewInt(int64(length)), nil)
|
|
i.Mod(i, t)
|
|
|
|
return i
|
|
}
|
|
|
|
// dynamicTruncation performs the dynamic Truncation for step 2 as defined in RFC4226
|
|
func dynamicTruncation(d []byte) *big.Int {
|
|
offset := d[len(d)-1] & 0xf
|
|
|
|
v := new(big.Int)
|
|
v.SetBytes(d[offset : offset+4])
|
|
|
|
mask := big.NewInt(0x7fffffff)
|
|
v.And(v, mask)
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
func (k *baseKey) check(code string, counter uint64) bool {
|
|
i := getOTP(k.opts.Secret, counter, k.opts.Length)
|
|
return subtle.ConstantTimeCompare([]byte(formatCode(i, uint(len(code)))), []byte(code)) == 1
|
|
}
|
|
|
|
func (k *baseKey) formatCode(i *big.Int) string {
|
|
return formatCode(i, k.opts.Length)
|
|
}
|
|
|
|
func formatCode(i *big.Int, length uint) string {
|
|
return fmt.Sprintf(fmt.Sprintf("%%0%dd", length), i.Int64())
|
|
}
|
|
|
|
// ValidateCustom allows validating an OTP type code without access to the key
|
|
// Also allows using a custom counter value - counter for HOTP or time since epoch for TOTP
|
|
func ValidateCustom(secret []byte, counter uint64, code string) bool {
|
|
k := baseKey{}
|
|
k.opts.Secret = secret
|
|
k.opts.Length = uint(len(code))
|
|
return k.check(code, counter)
|
|
}
|
|
|
|
// CustomCode creates and returns an OTP code with the given
|
|
func CustomCode(secret []byte, counter uint64, length uint) string {
|
|
i := getOTP(secret, counter, length)
|
|
return formatCode(i, length)
|
|
}
|