Adding otp code and updated README.

This commit is contained in:
Justin Judd 2016-01-11 08:03:04 +09:00
parent 4b54f2df3d
commit 01fd3e5886
10 changed files with 1063 additions and 1 deletions

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()
}
}