Initial import.
Basic HOTP functionality.
This commit is contained in:
40
hotp.go
Normal file
40
hotp.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package twofactor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/sha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HOTP struct {
|
||||||
|
*oath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (otp *HOTP) Type() Type {
|
||||||
|
return OATH_HOTP
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHOTP(key []byte, counter uint64, digits int) *HOTP {
|
||||||
|
return &HOTP{
|
||||||
|
oath: &oath{
|
||||||
|
key: key,
|
||||||
|
counter: counter,
|
||||||
|
size: digits,
|
||||||
|
hash: sha1.New,
|
||||||
|
algo: crypto.SHA1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (otp *HOTP) OTP() string {
|
||||||
|
code := otp.oath.OTP(otp.counter)
|
||||||
|
otp.counter++
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
func (otp *HOTP) URL(label string) string {
|
||||||
|
return otp.oath.URL(otp.Type(), label)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (otp *HOTP) SetProvider(provider string) {
|
||||||
|
otp.provider = provider
|
||||||
|
}
|
||||||
91
hotp_test.go
Normal file
91
hotp_test.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package twofactor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testKey = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
|
||||||
|
|
||||||
|
func newZeroHOTP() *HOTP {
|
||||||
|
return NewHOTP(testKey, 0, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sha1Hmac = []byte{
|
||||||
|
0x1f, 0x86, 0x98, 0x69, 0x0e,
|
||||||
|
0x02, 0xca, 0x16, 0x61, 0x85,
|
||||||
|
0x50, 0xef, 0x7f, 0x19, 0xda,
|
||||||
|
0x8e, 0x94, 0x5b, 0x55, 0x5a,
|
||||||
|
}
|
||||||
|
|
||||||
|
var truncExpect int64 = 0x50ef7f19
|
||||||
|
|
||||||
|
// This test runs through the truncation example given in the RFC.
|
||||||
|
func TestTruncate(t *testing.T) {
|
||||||
|
if result := truncate(sha1Hmac); result != truncExpect {
|
||||||
|
fmt.Printf("hotp: expected truncate -> %d, saw %d\n",
|
||||||
|
truncExpect, result)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
sha1Hmac[19]++
|
||||||
|
if result := truncate(sha1Hmac); result == truncExpect {
|
||||||
|
fmt.Println("hotp: expected truncation to fail")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rfcKey = []byte("12345678901234567890")
|
||||||
|
var rfcExpected = []string{
|
||||||
|
"755224",
|
||||||
|
"287082",
|
||||||
|
"359152",
|
||||||
|
"969429",
|
||||||
|
"338314",
|
||||||
|
"254676",
|
||||||
|
"287922",
|
||||||
|
"162583",
|
||||||
|
"399871",
|
||||||
|
"520489",
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test runs through the test cases presented in the RFC, and
|
||||||
|
// ensures that this implementation is in compliance.
|
||||||
|
func TestRFC(t *testing.T) {
|
||||||
|
otp := NewHOTP(rfcKey, 0, 6)
|
||||||
|
for i := 0; i < len(rfcExpected); i++ {
|
||||||
|
if otp.Counter() != uint64(i) {
|
||||||
|
fmt.Printf("hotp: invalid counter (should be %d, is %d",
|
||||||
|
i, otp.Counter())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
code := otp.OTP()
|
||||||
|
if code == "" {
|
||||||
|
fmt.Printf("hotp: failed to produce an OTP\n")
|
||||||
|
t.FailNow()
|
||||||
|
} else if code != rfcExpected[i] {
|
||||||
|
fmt.Printf("hotp: invalid OTP\n")
|
||||||
|
fmt.Printf("\tExpected: %s\n", rfcExpected[i])
|
||||||
|
fmt.Printf("\t Actual: %s\n", code)
|
||||||
|
fmt.Printf("\t Counter: %d\n", otp.counter)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test uses a different key than the test cases in the RFC,
|
||||||
|
// but runs through the same test cases to ensure that they fail as
|
||||||
|
// expected.
|
||||||
|
func TestBadRFC(t *testing.T) {
|
||||||
|
otp := NewHOTP(testKey, 0, 6)
|
||||||
|
for i := 0; i < len(rfcExpected); i++ {
|
||||||
|
code := otp.OTP()
|
||||||
|
if code == "" {
|
||||||
|
fmt.Printf("hotp: failed to produce an OTP\n")
|
||||||
|
t.FailNow()
|
||||||
|
} else if code == rfcExpected[i] {
|
||||||
|
fmt.Printf("hotp: should not have received a valid OTP\n")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
135
oath.go
Normal file
135
oath.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package twofactor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/hmac"
|
||||||
|
"encoding/base32"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultSize = 6
|
||||||
|
|
||||||
|
// oath provides a baseline struct for the two OATH algorithms.
|
||||||
|
type oath struct {
|
||||||
|
key []byte
|
||||||
|
counter uint64
|
||||||
|
size int
|
||||||
|
hash func() hash.Hash
|
||||||
|
algo crypto.Hash
|
||||||
|
provider string
|
||||||
|
}
|
||||||
|
|
||||||
|
// truncate contains the DT function from the RFC; this is used to
|
||||||
|
// deterministically select a sequence of 4 bytes from the HMAC
|
||||||
|
// counter hash.
|
||||||
|
func truncate(in []byte) int64 {
|
||||||
|
offset := int(in[len(in)-1] & 0xF)
|
||||||
|
p := in[offset : offset+4]
|
||||||
|
var binCode int32
|
||||||
|
binCode = int32((p[0] & 0x7f)) << 24
|
||||||
|
binCode += int32((p[1] & 0xff)) << 16
|
||||||
|
binCode += int32((p[2] & 0xff)) << 8
|
||||||
|
binCode += int32((p[3] & 0xff))
|
||||||
|
return int64(binCode) & 0x7FFFFFFF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o oath) Size() int {
|
||||||
|
return o.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o oath) Counter() uint64 {
|
||||||
|
return o.counter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o oath) SetCounter(counter uint64) {
|
||||||
|
o.counter = counter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o oath) Key() []byte {
|
||||||
|
return o.key[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o oath) Hash() func() hash.Hash {
|
||||||
|
return o.hash
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o oath) URL(t Type, label string) string {
|
||||||
|
secret := base32.StdEncoding.EncodeToString(o.key)
|
||||||
|
u := url.URL{}
|
||||||
|
v := url.Values{}
|
||||||
|
u.Scheme = "otpauth"
|
||||||
|
switch t {
|
||||||
|
case OATH_HOTP:
|
||||||
|
u.Host = "hotp"
|
||||||
|
case OATH_TOTP:
|
||||||
|
u.Host = "totp"
|
||||||
|
}
|
||||||
|
u.Path = label
|
||||||
|
v.Add("secret", secret)
|
||||||
|
if o.Counter() != 0 && t == OATH_HOTP {
|
||||||
|
v.Add("counter", fmt.Sprintf("%d", o.Counter()))
|
||||||
|
}
|
||||||
|
if o.Size() != defaultSize {
|
||||||
|
v.Add("digits", fmt.Sprintf("%d", o.Size()))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case o.algo == crypto.SHA256:
|
||||||
|
v.Add("algorithm", "SHA256")
|
||||||
|
case o.algo == crypto.SHA512:
|
||||||
|
v.Add("algorithm", "SHA512")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.provider != "" {
|
||||||
|
v.Add("provider", o.provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.RawQuery = v.Encode()
|
||||||
|
return u.String()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o oath) QR(label string) ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var digits = []int{
|
||||||
|
0: 1,
|
||||||
|
1: 10,
|
||||||
|
2: 100,
|
||||||
|
3: 1000,
|
||||||
|
4: 10000,
|
||||||
|
5: 100000,
|
||||||
|
6: 1000000,
|
||||||
|
7: 10000000,
|
||||||
|
8: 100000000,
|
||||||
|
9: 1000000000,
|
||||||
|
10: 10000000000,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The top-level type should provide a counter; for example, HOTP
|
||||||
|
// will provide the counter directly while TOTP will provide the
|
||||||
|
// time-stepped counter.
|
||||||
|
func (o oath) OTP(counter uint64) string {
|
||||||
|
var ctr [8]byte
|
||||||
|
binary.BigEndian.PutUint64(ctr[:], counter)
|
||||||
|
|
||||||
|
var mod int = 1
|
||||||
|
if len(digits) > o.size {
|
||||||
|
for i := 1; i <= o.size; i++ {
|
||||||
|
mod *= 10
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mod = digits[o.size]
|
||||||
|
}
|
||||||
|
|
||||||
|
h := hmac.New(o.hash, o.key)
|
||||||
|
h.Write(ctr[:])
|
||||||
|
dt := truncate(h.Sum(nil))
|
||||||
|
dt = dt % int64(mod)
|
||||||
|
fmtStr := fmt.Sprintf("%%%dd", o.size)
|
||||||
|
return fmt.Sprintf(fmtStr, dt)
|
||||||
|
}
|
||||||
61
otp.go
Normal file
61
otp.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package twofactor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Type uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
OATH_HOTP = iota
|
||||||
|
OATH_TOTP
|
||||||
|
)
|
||||||
|
|
||||||
|
var PRNG = rand.Reader
|
||||||
|
|
||||||
|
// Type OTP represents a one-time password token -- whether a
|
||||||
|
// software taken (as in the case of Google Authenticator) or a
|
||||||
|
// hardware token (as in the case of a YubiKey).
|
||||||
|
type OTP interface {
|
||||||
|
// Returns the current counter value; the meaning of the
|
||||||
|
// returned value is algorithm-specific.
|
||||||
|
Counter() uint64
|
||||||
|
|
||||||
|
// Set the counter to a specific value.
|
||||||
|
SetCounter(uint64)
|
||||||
|
|
||||||
|
// the secret key contained in the OTP
|
||||||
|
Key() []byte
|
||||||
|
|
||||||
|
// generate a new OTP
|
||||||
|
OTP() string
|
||||||
|
|
||||||
|
// the output size of the OTP
|
||||||
|
Size() int
|
||||||
|
|
||||||
|
// the hash function used by the OTP
|
||||||
|
Hash() func() hash.Hash
|
||||||
|
|
||||||
|
// URL generates a Google Authenticator url (or perhaps some other url)
|
||||||
|
URL(string) string
|
||||||
|
|
||||||
|
// QR outputs a byte slice containing a PNG-encoded QR code
|
||||||
|
// of the URL.
|
||||||
|
QR(string) ([]byte, error)
|
||||||
|
|
||||||
|
// Returns the type of this OTP.
|
||||||
|
Type() Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func OTPString(otp OTP) string {
|
||||||
|
var typeName string
|
||||||
|
switch otp.Type() {
|
||||||
|
case OATH_HOTP:
|
||||||
|
typeName = "OATH-HOTP"
|
||||||
|
case OATH_TOTP:
|
||||||
|
typeName = "OATH-TOTP"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s, %d", typeName, otp.Size())
|
||||||
|
}
|
||||||
13
otp_test.go
Normal file
13
otp_test.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package twofactor
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestHOTPString(t *testing.T) {
|
||||||
|
hotp := NewHOTP(nil, 0, 6)
|
||||||
|
hotpString := OTPString(hotp)
|
||||||
|
if hotpString != "OATH-HOTP, 6" {
|
||||||
|
fmt.Println("twofactor: invalid OTP string")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user