Adding otp code and updated README.
This commit is contained in:
parent
4b54f2df3d
commit
01fd3e5886
98
README.md
98
README.md
@ -1,3 +1,99 @@
|
||||
# otp
|
||||
|
||||
Go library for generating and using One Time Passwords.
|
||||
Go library for generating and using One Time Passwords. Supports both HOTP ([RFC4226](https://tools.ietf.org/html/rfc4226)) and TOTP ([RFC6238](https://tools.ietf.org/html/rfc6238)). Can be used for both server and client roles.
|
||||
|
||||
[![GoDoc](https://godoc.org/dev.justinjudd.org/justin/otp?status.svg)](https://godoc.org/dev.justinjudd.org/justin/otp)
|
||||
|
||||
## Examples
|
||||
|
||||
### OTP Server
|
||||
|
||||
import (
|
||||
"dev.justinjudd.org/justin/otp"
|
||||
)
|
||||
|
||||
var Issuer = "example.com"
|
||||
|
||||
func CreateKeyForUser(user string) {
|
||||
|
||||
opts := otp.NewHOTPKeyOptions()
|
||||
opts.Issuer = Issuer
|
||||
opts.Label = user
|
||||
key := otp.NewHOTPKey(opts)
|
||||
|
||||
// Store this string variable in your database
|
||||
keyURL := key.URL()
|
||||
|
||||
// Provide the URL to the customer so they can include it in their 2FA client.
|
||||
// Can email URL, or present QR code encoding of the URL
|
||||
}
|
||||
|
||||
func CheckUsersCode(user string, code string) (bool, error) {
|
||||
// Retrieve this string variable from your database
|
||||
var keyURL string
|
||||
|
||||
key, err := otp.FromURL(keyURL)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Ensure you are using the correct key
|
||||
if key.Label != user {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
success := key.Verify(code)
|
||||
|
||||
// Counter has been updated, update this info in the database
|
||||
// Don't need this step for TOTP keys as the counter is time-based
|
||||
keyURL = key.URL()
|
||||
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
|
||||
### OTP Client
|
||||
|
||||
import (
|
||||
"dev.justinjudd.org/justin/otp"
|
||||
)
|
||||
|
||||
// Just an example for storing OTP keys on the client
|
||||
var keys map[Key]string
|
||||
|
||||
|
||||
// Key is used as keys for the otp key storing map
|
||||
type Key struct {
|
||||
Issuer, Label string
|
||||
}
|
||||
|
||||
func GetCode(issuer, username string) (string, error) {
|
||||
|
||||
mapKey := Key{issuer, username}
|
||||
|
||||
// Get the stored Key URL
|
||||
keyURL, ok := keys[mapKey]
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Build the key from the URL
|
||||
key, err := otp.FromURL(keyURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Verify Issuer and Label are correct
|
||||
if key.Issuer != issuer || key.Label != username {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
code := key.OTP()
|
||||
|
||||
// If using HOTP, than need to save the state
|
||||
keyURL = key.URL()
|
||||
keys[mapKey]= keyURL
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
124
common.go
Normal file
124
common.go
Normal file
@ -0,0 +1,124 @@
|
||||
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)
|
||||
}
|
87
hotp.go
Normal file
87
hotp.go
Normal file
@ -0,0 +1,87 @@
|
||||
package otp
|
||||
|
||||
import "strconv"
|
||||
|
||||
// HOTPKeyOptions represents the settings or values to use when creating a HOTP Key
|
||||
type HOTPKeyOptions struct {
|
||||
KeyOptions
|
||||
Counter uint64
|
||||
}
|
||||
|
||||
// NewHOTPKeyOptions returns a HOTPKeyOptions using sane default values and a secret key
|
||||
func NewHOTPKeyOptions() HOTPKeyOptions {
|
||||
return HOTPKeyOptions{
|
||||
NewKeyOptions(),
|
||||
DefaultCounter,
|
||||
}
|
||||
}
|
||||
|
||||
// HOTPKey represents the HOTP family of OTP as found in RFC4226
|
||||
type HOTPKey struct {
|
||||
baseKey
|
||||
|
||||
counter uint64
|
||||
}
|
||||
|
||||
// NewHOTPKey creates and returns a new HOTP key
|
||||
// If no KeyOptions are provided, sane defaults and a random secret will be used
|
||||
func NewHOTPKey(o ...HOTPKeyOptions) Key {
|
||||
var opts HOTPKeyOptions
|
||||
if len(o) == 0 {
|
||||
opts = NewHOTPKeyOptions()
|
||||
} else {
|
||||
opts = o[0]
|
||||
}
|
||||
|
||||
h := &HOTPKey{}
|
||||
|
||||
h.baseKey = baseKey{}
|
||||
h.opts = opts.KeyOptions
|
||||
h.keyType = HOTP
|
||||
h.counter = opts.Counter
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// OTP produces a OTP code
|
||||
func (k *HOTPKey) OTP() string {
|
||||
|
||||
// need to increment counter so that code is different each time
|
||||
// Google Authenticator appears to increment before generating the code
|
||||
k.counter++
|
||||
|
||||
i := getOTP(k.Secret(), k.counter, k.Length())
|
||||
|
||||
return k.formatCode(i)
|
||||
|
||||
}
|
||||
|
||||
// URL creates a relevant URL to distribute/share the key as detailed at https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
func (k *HOTPKey) URL() string {
|
||||
u, vals := k.baseKey.URL()
|
||||
vals.Add("counter", strconv.FormatUint(k.counter, 10))
|
||||
u.RawQuery = vals.Encode()
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// Verify compares the provided code with different potential codes within the HOTPKeys allowed skew range
|
||||
func (k *HOTPKey) Verify(code string, counter ...uint64) bool {
|
||||
c := k.counter
|
||||
if len(counter) > 0 {
|
||||
c = counter[0]
|
||||
}
|
||||
|
||||
success := k.verify(c, code)
|
||||
if success {
|
||||
k.counter++
|
||||
}
|
||||
|
||||
return success
|
||||
|
||||
}
|
||||
|
||||
// IntegrityCheck provides information to verify the key is the same one used in Google Authenticator
|
||||
func (k *HOTPKey) IntegrityCheck() (string, uint64) {
|
||||
i := getOTP(k.Secret(), 0, k.Length())
|
||||
return k.formatCode(i), k.counter
|
||||
}
|
48
hotp_example_test.go
Normal file
48
hotp_example_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
package otp_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"dev.justinjudd.org/justin/otp"
|
||||
)
|
||||
|
||||
type Key struct {
|
||||
Issuer, Label string
|
||||
}
|
||||
|
||||
var keys map[Key]string
|
||||
|
||||
func CreateKey(issuer, username string) error {
|
||||
mapKey := Key{issuer, username}
|
||||
_, ok := keys[mapKey]
|
||||
if ok {
|
||||
return fmt.Errorf("Key already exists for Issuer:%s, Label:%s", issuer, username)
|
||||
}
|
||||
opts := otp.NewHOTPKeyOptions()
|
||||
opts.Issuer = issuer
|
||||
opts.Label = username
|
||||
k := otp.NewHOTPKey(opts)
|
||||
|
||||
keys[mapKey] = k.URL()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckCode(issuer, username, code string) bool {
|
||||
mapKey := Key{issuer, username}
|
||||
keyURL, ok := keys[mapKey]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
k, err := otp.FromURL(keyURL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return k.Verify(code)
|
||||
}
|
||||
|
||||
func Example_hOTP() {
|
||||
CreateKey("example.com", "user1")
|
||||
CheckCode("example.com", "user1", "451556")
|
||||
|
||||
}
|
218
hotp_test.go
Normal file
218
hotp_test.go
Normal file
@ -0,0 +1,218 @@
|
||||
package otp
|
||||
|
||||
import "testing"
|
||||
|
||||
var rfc4226Secret = "12345678901234567890"
|
||||
var rfc4226Codes = []string{
|
||||
"755224",
|
||||
"287082",
|
||||
"359152",
|
||||
"969429",
|
||||
"338314",
|
||||
"254676",
|
||||
"287922",
|
||||
"162583",
|
||||
"399871",
|
||||
"520489",
|
||||
}
|
||||
|
||||
var baseRFC4226URL = "otpauth://hotp/IETF:ReferenceImplementation?counter=0&digits=6&issuer=IETF&secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
|
||||
|
||||
func TestRFC4226OTP(t *testing.T) {
|
||||
|
||||
issuer := "IETF"
|
||||
label := "ReferenceImplementation"
|
||||
|
||||
o := NewHOTPKeyOptions()
|
||||
o.Secret = []byte(rfc4226Secret)
|
||||
o.Issuer = issuer
|
||||
o.Label = label
|
||||
h := NewHOTPKey(o)
|
||||
|
||||
code, counter := h.IntegrityCheck()
|
||||
if code != rfc4226Codes[counter] {
|
||||
t.Logf("Invalid HOTP code for counter %d", counter)
|
||||
t.Logf("\tExpected:\t%s\n", rfc4226Codes[counter])
|
||||
t.Logf("\tActual: \t%s\n", code)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
for counter, code := range rfc4226Codes[1:] {
|
||||
generated := h.OTP()
|
||||
if code != generated {
|
||||
t.Logf("Invalid HOTP code for counter %d", counter)
|
||||
t.Logf("\tExpected:\t%s\n", code)
|
||||
t.Logf("\tActual: \t%s\n", generated)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
if h.Issuer() != issuer {
|
||||
t.Log("Invalid Issuer found")
|
||||
t.Logf("\tExpected:\t%s\n", issuer)
|
||||
t.Logf("\tActual: \t%s\n", h.Issuer())
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if h.Label() != label {
|
||||
t.Log("Invalid Issuer found")
|
||||
t.Logf("\tExpected:\t%s\n", label)
|
||||
t.Logf("\tActual: \t%s\n", h.Label())
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if h.Type() != HOTP {
|
||||
t.Log("Invalid Key type found")
|
||||
t.Logf("\tExpected:\t%s\n", HOTP)
|
||||
t.Logf("\tActual: \t%s\n", h.Type())
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRFC4226CustomCounter(t *testing.T) {
|
||||
|
||||
issuer := "IETF"
|
||||
label := "ReferenceImplementation"
|
||||
|
||||
o := NewHOTPKeyOptions()
|
||||
o.Secret = []byte(rfc4226Secret)
|
||||
o.Issuer = issuer
|
||||
o.Label = label
|
||||
h := NewHOTPKey(o)
|
||||
|
||||
code, counter := h.IntegrityCheck()
|
||||
if code != rfc4226Codes[counter] {
|
||||
t.Logf("Invalid HOTP code for counter %d", counter)
|
||||
t.Logf("\tExpected:\t%s\n", rfc4226Codes[counter])
|
||||
t.Logf("\tActual: \t%s\n", code)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
for counter, code := range rfc4226Codes[1:] {
|
||||
if !h.Verify(code, uint64(counter)) {
|
||||
t.Logf("Invalid HOTP code for counter %d", counter)
|
||||
t.Logf("\tExpected:\t%s\n", code)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRFC4226FromURL(t *testing.T) {
|
||||
|
||||
h, err := FromURL(baseRFC4226URL)
|
||||
if err != nil {
|
||||
t.Log(err.Error())
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
k := h.(*HOTPKey)
|
||||
code, counter := k.IntegrityCheck()
|
||||
if code != rfc4226Codes[counter] {
|
||||
t.Logf("Invalid HOTP code for counter %d", counter)
|
||||
t.Logf("\tExpected:\t%s\n", rfc4226Codes[counter])
|
||||
t.Logf("\tActual: \t%s\n", code)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
for counter, code := range rfc4226Codes[1:] {
|
||||
generated := h.OTP()
|
||||
if code != generated {
|
||||
t.Logf("Invalid HOTP code for counter %d", counter)
|
||||
t.Logf("\tExpected:\t%s\n", code)
|
||||
t.Logf("\tActual: \t%s\n", generated)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRFC4226ToURL(t *testing.T) {
|
||||
|
||||
o := NewHOTPKeyOptions()
|
||||
o.Secret = []byte(rfc4226Secret)
|
||||
o.Issuer = "IETF"
|
||||
o.Label = "ReferenceImplementation"
|
||||
h := NewHOTPKey(o)
|
||||
|
||||
if h.URL() != baseRFC4226URL {
|
||||
t.Log("Invalid URL from RFC 4226 reference HOTP key")
|
||||
t.Logf("\tExpected:\t%s\n", baseRFC4226URL)
|
||||
t.Logf("\tActual: \t%s\n", h.URL())
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRFC4226Verify(t *testing.T) {
|
||||
|
||||
o := NewHOTPKeyOptions()
|
||||
o.Secret = []byte(rfc4226Secret)
|
||||
h := NewHOTPKey(o)
|
||||
|
||||
for counter, code := range rfc4226Codes {
|
||||
if !h.Verify(code) {
|
||||
t.Logf("Invalid HOTP code for counter %d", counter)
|
||||
t.Logf("\tExpected:\t%s\n", code)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRFC4226CustomValidate(t *testing.T) {
|
||||
secret := []byte(rfc4226Secret)
|
||||
for counter, code := range rfc4226Codes {
|
||||
|
||||
if !ValidateCustom(secret, uint64(counter), code) {
|
||||
t.Logf("Invalid HOTP code for counter %d", counter)
|
||||
t.Logf("\tExpected:\t%s\n", code)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRFC4226CustomCode(t *testing.T) {
|
||||
secret := []byte(rfc4226Secret)
|
||||
for counter, code := range rfc4226Codes {
|
||||
|
||||
generated := CustomCode(secret, uint64(counter), uint(len(code)))
|
||||
if code != generated {
|
||||
t.Logf("Invalid HOTP code for counter %d", counter)
|
||||
t.Logf("\tExpected:\t%s\n", code)
|
||||
t.Logf("\tActual: \t%s\n", generated)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestHOTPVerify(t *testing.T) {
|
||||
k := NewHOTPKey()
|
||||
first := k.OTP()
|
||||
if !k.Verify(first) { //successful verify increments counter
|
||||
t.Logf("HOTP key %s failed to properly verify the first code - %s", k.URL(), first)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if !k.Verify(first) { //successful verify increments counter
|
||||
t.Logf("HOTP key %s failed to properly verify the first code - %s", k.URL(), first)
|
||||
t.Logf("First code should still be verified because of AllowedSkew")
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if k.Verify(first) { //Key should have advanced to the next code
|
||||
t.Logf("HOTP key %s should not have verified the first code - %s", k.URL(), first)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
k.SetSkew(3)
|
||||
if !k.Verify(first) { //successful verify increments counter
|
||||
t.Logf("HOTP key %s failed to properly verify the first code - %s", k.URL(), first)
|
||||
t.Logf("First code should still be verified because of AllowedSkew")
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
}
|
212
otp.go
Normal file
212
otp.go
Normal file
@ -0,0 +1,212 @@
|
||||
// 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
|
||||
|
||||
}
|
36
otp_test.go
Normal file
36
otp_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
package otp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFromURL(t *testing.T) {
|
||||
errorUrls := map[string]string{
|
||||
"garbage": ":",
|
||||
"badScheme": "http://hotp/example.com:test?counter=0&digits=6&issuer=example.com&secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ",
|
||||
"badKey": "otpauth://motp/example.com:test?counter=0&digits=6&issuer=example.com&secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ",
|
||||
"noSecret": "otpauth://hotp/example.com:test?counter=0&digits=6&issuer=example.com",
|
||||
}
|
||||
|
||||
otherUrls := map[string]string{
|
||||
"badDigits": "otpauth://hotp/example.com:test?counter=0&digits=six&issuer=example.com&secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ",
|
||||
}
|
||||
|
||||
for name, url := range errorUrls {
|
||||
_, err := FromURL(url)
|
||||
if err == nil {
|
||||
t.Logf("Error expected for %s url - %s", name, url)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
for name, url := range otherUrls {
|
||||
_, err := FromURL(url)
|
||||
if err != nil {
|
||||
t.Logf("Error not expected for %s url - %s", name, url)
|
||||
t.Logf("\t %s", err.Error())
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
91
totp.go
Normal file
91
totp.go
Normal file
@ -0,0 +1,91 @@
|
||||
package otp
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TOTPKeyOptions represents the settings or values to use when creating a TOTP Key
|
||||
type TOTPKeyOptions struct {
|
||||
KeyOptions
|
||||
Period uint64
|
||||
}
|
||||
|
||||
// NewTOTPKeyOptions returns a TOTPKeyOptions using sane default values and a secret key
|
||||
func NewTOTPKeyOptions() TOTPKeyOptions {
|
||||
return TOTPKeyOptions{
|
||||
NewKeyOptions(),
|
||||
DefaultPeriod,
|
||||
}
|
||||
}
|
||||
|
||||
// TOTPKey represents the TOTP family of OTP as defined in RFC6238
|
||||
type TOTPKey struct {
|
||||
baseKey
|
||||
|
||||
period uint64
|
||||
}
|
||||
|
||||
// NewTOTPKey creates and returns a new TOTP key
|
||||
// If no KeyOptions are provided, sane defaults and a random secret will be used
|
||||
func NewTOTPKey(o ...TOTPKeyOptions) Key {
|
||||
var opts TOTPKeyOptions
|
||||
if len(o) == 0 {
|
||||
opts = NewTOTPKeyOptions()
|
||||
} else {
|
||||
opts = o[0]
|
||||
}
|
||||
|
||||
t := &TOTPKey{}
|
||||
t.baseKey = baseKey{}
|
||||
t.opts = opts.KeyOptions
|
||||
t.keyType = TOTP
|
||||
|
||||
t.period = opts.Period
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Period returns the time period this TOTP Key uses - default is 30 seconds
|
||||
func (k *TOTPKey) Period() uint64 {
|
||||
return k.period
|
||||
}
|
||||
|
||||
func (k *TOTPKey) timeToCounter(t time.Time) uint64 {
|
||||
return TimeToCounter(t, k.period)
|
||||
}
|
||||
|
||||
// TimeToCounter converts a provided time to a counter to be used for the OTP
|
||||
func TimeToCounter(t time.Time, period uint64) uint64 {
|
||||
return uint64(t.Unix() / int64(period))
|
||||
}
|
||||
|
||||
// OTP produces a one-time use code
|
||||
func (k *TOTPKey) OTP() string {
|
||||
i := getOTP(k.Secret(), k.timeToCounter(time.Now()), k.Length())
|
||||
|
||||
return k.formatCode(i)
|
||||
}
|
||||
|
||||
// URL creates a relevant URL to distribute/share the key as detailed at https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
func (k *TOTPKey) URL() string {
|
||||
u, vals := k.baseKey.URL()
|
||||
vals.Add("period", strconv.FormatUint(k.period, 10))
|
||||
u.RawQuery = vals.Encode()
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// Verify compares the provided code with different potential codes within the allowed skew range. Counter should be Seconds since the Unix Epoch
|
||||
func (k *TOTPKey) Verify(code string, counter ...uint64) bool {
|
||||
c := k.timeToCounter(time.Now())
|
||||
if len(counter) > 0 {
|
||||
c = counter[0] / k.period
|
||||
}
|
||||
return k.verify(c, code)
|
||||
}
|
||||
|
||||
// IntegrityCheck provides information to verify the key is the same one used in Google Authenticator - doesn't appear to be used for TOTP keys though
|
||||
func (k *TOTPKey) IntegrityCheck() (string, uint64) {
|
||||
i := getOTP(k.Secret(), 0, k.Length())
|
||||
return k.formatCode(i), k.timeToCounter(time.Now())
|
||||
}
|
15
totp_example_test.go
Normal file
15
totp_example_test.go
Normal file
@ -0,0 +1,15 @@
|
||||
package otp_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"dev.justinjudd.org/justin/otp"
|
||||
)
|
||||
|
||||
func Example_tOTP() {
|
||||
o := otp.NewTOTPKeyOptions()
|
||||
o.Secret = []byte("Some Secret")
|
||||
|
||||
k := otp.NewTOTPKey(o)
|
||||
fmt.Println(k.OTP())
|
||||
}
|
135
totp_test.go
Normal file
135
totp_test.go
Normal file
@ -0,0 +1,135 @@
|
||||
package otp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var rfc6238Secret = "12345678901234567890"
|
||||
|
||||
var rfc6238CounterCodes = map[uint64]string{
|
||||
59: "94287082",
|
||||
1111111109: "07081804",
|
||||
1111111111: "14050471",
|
||||
1234567890: "89005924",
|
||||
2000000000: "69279037",
|
||||
20000000000: "65353130",
|
||||
}
|
||||
|
||||
var baseRFC6238URL = "otpauth://totp/IETF:ReferenceImplementation?digits=6&issuer=IETF&period=30&secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
|
||||
|
||||
func TestRFC6238CustomCode(t *testing.T) {
|
||||
secret := []byte(rfc6238Secret)
|
||||
for counter, code := range rfc6238CounterCodes {
|
||||
generated := CustomCode(secret, counter/DefaultPeriod, uint(len(code)))
|
||||
if code != generated {
|
||||
t.Logf("Invalid TOTP code for counter %d", counter)
|
||||
t.Logf("\tExpected:\t%s\n", code)
|
||||
t.Logf("\tActual: \t%s\n", generated)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRFC6238Verify(t *testing.T) {
|
||||
o := NewTOTPKeyOptions()
|
||||
o.Secret = []byte(rfc6238Secret)
|
||||
o.Length = 8
|
||||
k := NewTOTPKey(o)
|
||||
|
||||
for counter, code := range rfc6238CounterCodes {
|
||||
if !k.Verify(code, counter) {
|
||||
t.Logf("Invalid TOTP code for counter %d", counter)
|
||||
t.Logf("\tExpected:\t%s\n", code)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
tKey := k.(*TOTPKey)
|
||||
code, counter := k.IntegrityCheck()
|
||||
expectedCounter := uint64(time.Now().Unix()) / tKey.Period()
|
||||
if expectedCounter-counter > 1 {
|
||||
t.Logf("Invalid time returned for TOTP Integrity check")
|
||||
t.Logf("\tExpected:\t%s\n", expectedCounter)
|
||||
t.Logf("\tActual: \t%s\n", counter)
|
||||
t.FailNow()
|
||||
}
|
||||
if !k.Verify(code, 0) {
|
||||
t.Logf("Invalid initial code returned for TOTP Integrity check")
|
||||
t.Logf("\tExpected:\t%s\n", code)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRFC6238CustomValidate(t *testing.T) {
|
||||
secret := []byte(rfc6238Secret)
|
||||
for counter, code := range rfc6238CounterCodes {
|
||||
|
||||
if !ValidateCustom(secret, counter/DefaultPeriod, code) {
|
||||
t.Logf("Invalid HOTP code for counter %d", counter)
|
||||
t.Logf("\tExpected:\t%s\n", code)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRFC6238FromURL(t *testing.T) {
|
||||
|
||||
k, err := FromURL(baseRFC6238URL)
|
||||
if err != nil {
|
||||
t.Log(err.Error())
|
||||
t.FailNow()
|
||||
}
|
||||
if k.URL() != baseRFC6238URL {
|
||||
t.Log("Invalid URL from RFC 6238 reference TOTP key")
|
||||
t.Logf("\tExpected:\t%s\n", baseRFC6238URL)
|
||||
t.Logf("\tActual: \t%s\n", k.URL())
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRFC6238ToURL(t *testing.T) {
|
||||
|
||||
o := NewTOTPKeyOptions()
|
||||
o.Secret = []byte(rfc6238Secret)
|
||||
o.Issuer = "IETF"
|
||||
o.Label = "ReferenceImplementation"
|
||||
k := NewTOTPKey(o)
|
||||
|
||||
if k.URL() != baseRFC6238URL {
|
||||
t.Log("Invalid URL from RFC 6238 reference TOTP key")
|
||||
t.Logf("\tExpected:\t%s\n", baseRFC6238URL)
|
||||
t.Logf("\tActual: \t%s\n", k.URL())
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestTOTPVerify(t *testing.T) {
|
||||
k := NewTOTPKey()
|
||||
first := k.OTP()
|
||||
if !k.Verify(first) {
|
||||
t.Logf("TOTP key %s failed to properly verify the first code - %s", k.URL(), first)
|
||||
t.FailNow()
|
||||
}
|
||||
// Need to wait a full period
|
||||
newTime := uint64(time.Now().Add(time.Duration(DefaultPeriod) * time.Second).Unix())
|
||||
|
||||
// Should still validate because of Allowed Skew
|
||||
if !k.Verify(first) {
|
||||
t.Logf("TOTP key %s failed to properly verify the first code - %s", k.URL(), first)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
// wait another period
|
||||
newTime = uint64(time.Now().Add(time.Duration(DefaultPeriod) * time.Second * 2).Unix())
|
||||
if k.Verify(first, newTime) { //Key should have advanced to the next code
|
||||
t.Logf("TOTP key %s should not still verify the first code - %s", k.URL(), first)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user