diff --git a/README.md b/README.md index ae1cee6..44177aa 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,99 @@ # otp -Go library for generating and using One Time Passwords. \ No newline at end of file +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 + } diff --git a/common.go b/common.go new file mode 100644 index 0000000..0ef0b13 --- /dev/null +++ b/common.go @@ -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) +} diff --git a/hotp.go b/hotp.go new file mode 100644 index 0000000..b1167a1 --- /dev/null +++ b/hotp.go @@ -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 +} diff --git a/hotp_example_test.go b/hotp_example_test.go new file mode 100644 index 0000000..7f04f91 --- /dev/null +++ b/hotp_example_test.go @@ -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") + +} diff --git a/hotp_test.go b/hotp_test.go new file mode 100644 index 0000000..95f0127 --- /dev/null +++ b/hotp_test.go @@ -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() + } + +} diff --git a/otp.go b/otp.go new file mode 100644 index 0000000..c5a3e3a --- /dev/null +++ b/otp.go @@ -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 + +} diff --git a/otp_test.go b/otp_test.go new file mode 100644 index 0000000..73dda32 --- /dev/null +++ b/otp_test.go @@ -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() + } + } + +} diff --git a/totp.go b/totp.go new file mode 100644 index 0000000..fa6f1ea --- /dev/null +++ b/totp.go @@ -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()) +} diff --git a/totp_example_test.go b/totp_example_test.go new file mode 100644 index 0000000..1dd2da6 --- /dev/null +++ b/totp_example_test.go @@ -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()) +} diff --git a/totp_test.go b/totp_test.go new file mode 100644 index 0000000..d8158ab --- /dev/null +++ b/totp_test.go @@ -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() + } + +}