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
|
# 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