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. | ||||
| 
 | ||||
| [](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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Justin Judd
						Justin Judd