otp/common.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)
}