213 lines
4.8 KiB
Go
213 lines
4.8 KiB
Go
// Package otp provides support for HOTP (RFC4226) and TOTP (RFC6238) One-Time Passwords
|
|
package otp
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"encoding/base32"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Key is an interface that defines the charactaristics that specific implementations of OTP Keys must have
|
|
type Key interface {
|
|
Type() Type
|
|
Label() string
|
|
Issuer() string
|
|
Length() uint
|
|
AllowedSkew() uint64
|
|
|
|
EncodeSecret() string
|
|
DecodeSecret(string) error
|
|
Secret() []byte
|
|
URL() string
|
|
|
|
SetSkew(deltas uint64)
|
|
|
|
// OTP generates and returns the next One-Time Password to be used
|
|
OTP() string
|
|
// Verify checks if the provided code(OTP) is acceptable. Takes an optional counter to bypass the counter set in the key
|
|
Verify(code string, counter ...uint64) bool
|
|
//IntegrityCheck returns the base (counter @ 0) OTP code and the current counter value
|
|
IntegrityCheck() (string, uint64)
|
|
}
|
|
|
|
// KeyOptions stores the settings that can be used in creating keys
|
|
type KeyOptions struct {
|
|
Secret []byte
|
|
Length uint
|
|
|
|
Label string
|
|
Issuer string
|
|
|
|
AllowedSkew uint64
|
|
}
|
|
|
|
// NewKeyOptions creates and returns KeyOptions using the default values and a random secret
|
|
func NewKeyOptions() KeyOptions {
|
|
o := KeyOptions{}
|
|
o.Issuer = DefaultIssuer
|
|
o.AllowedSkew = DefaultAllowedSkew
|
|
o.Secret = make([]byte, sha1.Size)
|
|
rand.Read(o.Secret)
|
|
o.Length = DefaultLength
|
|
o.Label = DefaultLabel
|
|
|
|
return o
|
|
}
|
|
|
|
type baseKey struct {
|
|
opts KeyOptions
|
|
|
|
keyType Type
|
|
}
|
|
|
|
// Issuer returns the Issuer of the key
|
|
func (k *baseKey) Issuer() string {
|
|
return k.opts.Issuer
|
|
}
|
|
|
|
// Label returns the label or username of the key
|
|
func (k *baseKey) Label() string {
|
|
return k.opts.Label
|
|
}
|
|
|
|
// Type returns the Type value of the key - should be HOTP or TOTP
|
|
func (k *baseKey) Type() Type {
|
|
return k.keyType
|
|
}
|
|
|
|
// Secret returns the keys secret
|
|
func (k *baseKey) Secret() []byte {
|
|
return k.opts.Secret
|
|
}
|
|
|
|
// AllowedSkew returns the value of how many codes before and after the current code should be checked
|
|
func (k *baseKey) AllowedSkew() uint64 {
|
|
return k.opts.AllowedSkew
|
|
}
|
|
|
|
// SetSkew sets the value of how many codes both before and after the current code should be checked for code verification
|
|
func (k *baseKey) SetSkew(delta uint64) {
|
|
k.opts.AllowedSkew = delta
|
|
}
|
|
|
|
// Length returns the digit length of codes that will be generated
|
|
func (k *baseKey) Length() uint {
|
|
return k.opts.Length
|
|
}
|
|
|
|
// EncodedSecret converts the secret to base32 encoding
|
|
func (k *baseKey) EncodeSecret() string {
|
|
return base32.StdEncoding.EncodeToString(k.opts.Secret)
|
|
}
|
|
|
|
// DecodeSecret converts an encoded string to the secret byte value and sets it to the keys secret
|
|
func (k *baseKey) DecodeSecret(s string) error {
|
|
var err error
|
|
k.opts.Secret, err = base32.StdEncoding.DecodeString(s)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
func (k *baseKey) verify(counter uint64, code string) bool {
|
|
counters := []uint64{counter}
|
|
var i uint64
|
|
// Create slice of counters within the allowed skew range
|
|
for i = 1; i <= k.AllowedSkew(); i++ {
|
|
counters = append(counters, counter+i)
|
|
counters = append(counters, counter-i)
|
|
|
|
}
|
|
// As soon as one of the counters generates the same code, the code is verified valid
|
|
for _, c := range counters {
|
|
if k.check(code, c) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// URL converts a key to URL format as detailed at https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
|
func (k *baseKey) URL() (url.URL, url.Values) {
|
|
u := url.URL{}
|
|
vals := url.Values{}
|
|
|
|
u.Scheme = Scheme
|
|
u.Host = k.keyType.String()
|
|
u.Path = k.opts.Issuer + ":" + k.opts.Label
|
|
vals.Add("secret", k.EncodeSecret())
|
|
vals.Add("digits", strconv.FormatUint(uint64(k.opts.Length), 10))
|
|
if len(k.opts.Issuer) != 0 {
|
|
vals.Add("issuer", k.opts.Issuer)
|
|
}
|
|
|
|
return u, vals
|
|
}
|
|
|
|
// FromURL parses an OTP URL and creates the relevant Key
|
|
// The URL/URI is specified at https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
|
func FromURL(URL string) (k Key, err error) {
|
|
u, err := url.Parse(URL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
//Verify URL
|
|
if u.Scheme != Scheme {
|
|
return nil, Error("Invalid scheme found")
|
|
}
|
|
t := NewType(u.Host)
|
|
if t == UnknownType {
|
|
return nil, Error("Invalid type found")
|
|
}
|
|
|
|
vals := u.Query()
|
|
|
|
b := baseKey{}
|
|
|
|
b.keyType = t
|
|
|
|
s := vals.Get("secret")
|
|
if len(s) == 0 {
|
|
return nil, Error("Secret not found")
|
|
}
|
|
err = b.DecodeSecret(s)
|
|
|
|
d, err := strconv.ParseUint(vals.Get("digits"), 0, 0)
|
|
if err == nil {
|
|
b.opts.Length = uint(d)
|
|
} else {
|
|
b.opts.Length = DefaultLength
|
|
}
|
|
|
|
b.opts.Issuer = vals.Get("issuer")
|
|
parts := strings.Split(u.Path, ":")
|
|
b.opts.Label = parts[len(parts)-1]
|
|
|
|
// Algorithm is an available parameter - maybe it will be used in the future
|
|
|
|
switch t {
|
|
case HOTP:
|
|
h := HOTPKey{}
|
|
h.baseKey = b
|
|
h.counter, _ = strconv.ParseUint(vals.Get("counter"), 0, 0)
|
|
|
|
k = &h
|
|
|
|
case TOTP:
|
|
t := TOTPKey{}
|
|
t.baseKey = b
|
|
t.period, _ = strconv.ParseUint(vals.Get("period"), 0, 0)
|
|
|
|
k = &t
|
|
|
|
}
|
|
|
|
return k, nil
|
|
|
|
}
|