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