HOTP and TOTP-SHA-1 working.
why the frak aren't the SHA-256 and SHA-512 variants working
This commit is contained in:
51
hotp.go
51
hotp.go
@@ -3,6 +3,10 @@ package twofactor
|
|||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
"encoding/base32"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HOTP struct {
|
type HOTP struct {
|
||||||
@@ -38,3 +42,50 @@ func (otp *HOTP) URL(label string) string {
|
|||||||
func (otp *HOTP) SetProvider(provider string) {
|
func (otp *HOTP) SetProvider(provider string) {
|
||||||
otp.provider = provider
|
otp.provider = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateGoogleHOTP() *HOTP {
|
||||||
|
key := make([]byte, sha1.Size)
|
||||||
|
if _, err := io.ReadFull(PRNG, key); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return NewHOTP(key, 0, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hotpFromURL(u *url.URL) (*HOTP, string, error) {
|
||||||
|
label := u.Path[1:]
|
||||||
|
v := u.Query()
|
||||||
|
|
||||||
|
secret := v.Get("secret")
|
||||||
|
if secret == "" {
|
||||||
|
return nil, "", ErrInvalidURL
|
||||||
|
}
|
||||||
|
|
||||||
|
var digits = 6
|
||||||
|
if sdigit := v.Get("digits"); sdigit != "" {
|
||||||
|
tmpDigits, err := strconv.ParseInt(sdigit, 10, 8)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
digits = int(tmpDigits)
|
||||||
|
}
|
||||||
|
|
||||||
|
var counter uint64 = 0
|
||||||
|
if scounter := v.Get("counter"); scounter != "" {
|
||||||
|
var err error
|
||||||
|
counter, err = strconv.ParseUint(scounter, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := base32.StdEncoding.DecodeString(secret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
otp := NewHOTP(key, counter, digits)
|
||||||
|
return otp, label, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (otp *HOTP) QR(label string) ([]byte, error) {
|
||||||
|
return otp.oath.QR(otp.Type(), label)
|
||||||
|
}
|
||||||
|
|||||||
54
hotp_test.go
54
hotp_test.go
@@ -11,32 +11,8 @@ func newZeroHOTP() *HOTP {
|
|||||||
return NewHOTP(testKey, 0, 6)
|
return NewHOTP(testKey, 0, 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sha1Hmac = []byte{
|
var rfcHotpKey = []byte("12345678901234567890")
|
||||||
0x1f, 0x86, 0x98, 0x69, 0x0e,
|
var rfcHotpExpected = []string{
|
||||||
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",
|
"755224",
|
||||||
"287082",
|
"287082",
|
||||||
"359152",
|
"359152",
|
||||||
@@ -51,21 +27,21 @@ var rfcExpected = []string{
|
|||||||
|
|
||||||
// This test runs through the test cases presented in the RFC, and
|
// This test runs through the test cases presented in the RFC, and
|
||||||
// ensures that this implementation is in compliance.
|
// ensures that this implementation is in compliance.
|
||||||
func TestRFC(t *testing.T) {
|
func TestHotpRFC(t *testing.T) {
|
||||||
otp := NewHOTP(rfcKey, 0, 6)
|
otp := NewHOTP(rfcHotpKey, 0, 6)
|
||||||
for i := 0; i < len(rfcExpected); i++ {
|
for i := 0; i < len(rfcHotpExpected); i++ {
|
||||||
if otp.Counter() != uint64(i) {
|
if otp.Counter() != uint64(i) {
|
||||||
fmt.Printf("hotp: invalid counter (should be %d, is %d",
|
fmt.Printf("twofactor: invalid counter (should be %d, is %d",
|
||||||
i, otp.Counter())
|
i, otp.Counter())
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
code := otp.OTP()
|
code := otp.OTP()
|
||||||
if code == "" {
|
if code == "" {
|
||||||
fmt.Printf("hotp: failed to produce an OTP\n")
|
fmt.Printf("twofactor: failed to produce an OTP\n")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
} else if code != rfcExpected[i] {
|
} else if code != rfcHotpExpected[i] {
|
||||||
fmt.Printf("hotp: invalid OTP\n")
|
fmt.Printf("twofactor: invalid OTP\n")
|
||||||
fmt.Printf("\tExpected: %s\n", rfcExpected[i])
|
fmt.Printf("\tExpected: %s\n", rfcHotpExpected[i])
|
||||||
fmt.Printf("\t Actual: %s\n", code)
|
fmt.Printf("\t Actual: %s\n", code)
|
||||||
fmt.Printf("\t Counter: %d\n", otp.counter)
|
fmt.Printf("\t Counter: %d\n", otp.counter)
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
@@ -76,15 +52,15 @@ func TestRFC(t *testing.T) {
|
|||||||
// This test uses a different key than the test cases in the RFC,
|
// 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
|
// but runs through the same test cases to ensure that they fail as
|
||||||
// expected.
|
// expected.
|
||||||
func TestBadRFC(t *testing.T) {
|
func TestHotpBadRFC(t *testing.T) {
|
||||||
otp := NewHOTP(testKey, 0, 6)
|
otp := NewHOTP(testKey, 0, 6)
|
||||||
for i := 0; i < len(rfcExpected); i++ {
|
for i := 0; i < len(rfcHotpExpected); i++ {
|
||||||
code := otp.OTP()
|
code := otp.OTP()
|
||||||
if code == "" {
|
if code == "" {
|
||||||
fmt.Printf("hotp: failed to produce an OTP\n")
|
fmt.Printf("twofactor: failed to produce an OTP\n")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
} else if code == rfcExpected[i] {
|
} else if code == rfcHotpExpected[i] {
|
||||||
fmt.Printf("hotp: should not have received a valid OTP\n")
|
fmt.Printf("twofactor: should not have received a valid OTP\n")
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
53
oath.go
53
oath.go
@@ -1,6 +1,7 @@
|
|||||||
package twofactor
|
package twofactor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"code.google.com/p/rsc/qr"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
@@ -22,20 +23,6 @@ type oath struct {
|
|||||||
provider string
|
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 {
|
func (o oath) Size() int {
|
||||||
return o.size
|
return o.size
|
||||||
}
|
}
|
||||||
@@ -92,11 +79,7 @@ func (o oath) URL(t Type, label string) string {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o oath) QR(label string) ([]byte, error) {
|
var digits = []int64{
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var digits = []int{
|
|
||||||
0: 1,
|
0: 1,
|
||||||
1: 10,
|
1: 10,
|
||||||
2: 100,
|
2: 100,
|
||||||
@@ -117,7 +100,7 @@ func (o oath) OTP(counter uint64) string {
|
|||||||
var ctr [8]byte
|
var ctr [8]byte
|
||||||
binary.BigEndian.PutUint64(ctr[:], counter)
|
binary.BigEndian.PutUint64(ctr[:], counter)
|
||||||
|
|
||||||
var mod int = 1
|
var mod int64 = 1
|
||||||
if len(digits) > o.size {
|
if len(digits) > o.size {
|
||||||
for i := 1; i <= o.size; i++ {
|
for i := 1; i <= o.size; i++ {
|
||||||
mod *= 10
|
mod *= 10
|
||||||
@@ -128,8 +111,32 @@ func (o oath) OTP(counter uint64) string {
|
|||||||
|
|
||||||
h := hmac.New(o.hash, o.key)
|
h := hmac.New(o.hash, o.key)
|
||||||
h.Write(ctr[:])
|
h.Write(ctr[:])
|
||||||
dt := truncate(h.Sum(nil))
|
dt := truncate(h.Sum(nil)) % mod
|
||||||
dt = dt % int64(mod)
|
fmtStr := fmt.Sprintf("%%0%dd", o.size)
|
||||||
fmtStr := fmt.Sprintf("%%%dd", o.size)
|
|
||||||
return fmt.Sprintf(fmtStr, dt)
|
return fmt.Sprintf(fmtStr, dt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// QR generates a byte slice containing the a QR code encoded as a
|
||||||
|
// PNG with level Q error correction.
|
||||||
|
func (o oath) QR(t Type, label string) ([]byte, error) {
|
||||||
|
u := o.URL(t, label)
|
||||||
|
code, err := qr.Encode(u, qr.Q)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return code.PNG(), nil
|
||||||
|
}
|
||||||
|
|||||||
29
otp.go
29
otp.go
@@ -2,8 +2,10 @@ package twofactor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Type uint
|
type Type uint
|
||||||
@@ -13,8 +15,15 @@ const (
|
|||||||
OATH_TOTP
|
OATH_TOTP
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PRNG is an io.Reader that provides a cryptographically secure
|
||||||
|
// random byte stream.
|
||||||
var PRNG = rand.Reader
|
var PRNG = rand.Reader
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidURL = errors.New("twofactor: invalid URL")
|
||||||
|
ErrInvalidAlgo = errors.New("twofactor: invalid algorithm")
|
||||||
|
)
|
||||||
|
|
||||||
// Type OTP represents a one-time password token -- whether a
|
// Type OTP represents a one-time password token -- whether a
|
||||||
// software taken (as in the case of Google Authenticator) or a
|
// software taken (as in the case of Google Authenticator) or a
|
||||||
// hardware token (as in the case of a YubiKey).
|
// hardware token (as in the case of a YubiKey).
|
||||||
@@ -59,3 +68,23 @@ func OTPString(otp OTP) string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%s, %d", typeName, otp.Size())
|
return fmt.Sprintf("%s, %d", typeName, otp.Size())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FromURL(URL string) (OTP, string, error) {
|
||||||
|
u, err := url.Parse(URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Scheme != "otpauth" {
|
||||||
|
return nil, "", ErrInvalidURL
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case u.Host == "totp":
|
||||||
|
return totpFromURL(u)
|
||||||
|
case u.Host == "hotp":
|
||||||
|
return hotpFromURL(u)
|
||||||
|
default:
|
||||||
|
return nil, "", ErrInvalidURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
84
otp_test.go
84
otp_test.go
@@ -1,6 +1,7 @@
|
|||||||
package twofactor
|
package twofactor
|
||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
import "io"
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestHOTPString(t *testing.T) {
|
func TestHOTPString(t *testing.T) {
|
||||||
@@ -11,3 +12,86 @@ func TestHOTPString(t *testing.T) {
|
|||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This test generates a new OTP, outputs the URL for that OTP,
|
||||||
|
// and attempts to parse that URL. It verifies that the two OTPs
|
||||||
|
// are the same, and that they produce the same output.
|
||||||
|
func TestURL(t *testing.T) {
|
||||||
|
var ident = "testuser@foo"
|
||||||
|
otp := NewHOTP(testKey, 0, 6)
|
||||||
|
url := otp.URL("testuser@foo")
|
||||||
|
otp2, id, err := FromURL(url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("hotp: failed to parse HOTP URL\n")
|
||||||
|
t.FailNow()
|
||||||
|
} else if id != ident {
|
||||||
|
fmt.Printf("hotp: bad label\n")
|
||||||
|
fmt.Printf("\texpected: %s\n", ident)
|
||||||
|
fmt.Printf("\t actual: %s\n", id)
|
||||||
|
t.FailNow()
|
||||||
|
} else if otp2.Counter() != otp.Counter() {
|
||||||
|
fmt.Printf("hotp: OTP counters aren't synced\n")
|
||||||
|
fmt.Printf("\toriginal: %d\n", otp.Counter())
|
||||||
|
fmt.Printf("\t second: %d\n", otp2.Counter())
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
code1 := otp.OTP()
|
||||||
|
code2 := otp2.OTP()
|
||||||
|
if code1 != code2 {
|
||||||
|
fmt.Printf("hotp: mismatched OTPs\n")
|
||||||
|
fmt.Printf("\texpected: %s\n", code1)
|
||||||
|
fmt.Printf("\t actual: %s\n", code2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// There's not much we can do test the QR code, except to
|
||||||
|
// ensure it doesn't fail.
|
||||||
|
_, err = otp.QR(ident)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("hotp: failed to generate QR code PNG (%v)\n", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should fail because the maximum size of an alphanumeric
|
||||||
|
// QR code with the lowest-level of error correction should
|
||||||
|
// max out at 4296 bytes. 8k may be a bit overkill... but it
|
||||||
|
// gets the job done. The value is read from the PRNG to
|
||||||
|
// increase the likelihood that the returned data is
|
||||||
|
// uncompressible.
|
||||||
|
var tooBigIdent = make([]byte, 8192)
|
||||||
|
_, err = io.ReadFull(PRNG, tooBigIdent)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("hotp: failed to read identity (%v)\n", err)
|
||||||
|
t.FailNow()
|
||||||
|
} else if _, err = otp.QR(string(tooBigIdent)); err == nil {
|
||||||
|
fmt.Println("hotp: QR code should fail to encode oversized URL")
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test attempts a variety of invalid urls against the parser
|
||||||
|
// to ensure they fail.
|
||||||
|
func TestBadURL(t *testing.T) {
|
||||||
|
var urlList = []string{
|
||||||
|
"http://google.com",
|
||||||
|
"",
|
||||||
|
"-",
|
||||||
|
"foo",
|
||||||
|
"otpauth:/foo/bar/baz",
|
||||||
|
"://",
|
||||||
|
"otpauth://hotp/secret=bar",
|
||||||
|
"otpauth://hotp/?secret=QUJDRA&algorithm=SHA256",
|
||||||
|
"otpauth://hotp/?digits=",
|
||||||
|
"otpauth://hotp/?secret=123",
|
||||||
|
"otpauth://hotp/?secret=MFRGGZDF&digits=ABCD",
|
||||||
|
"otpauth://hotp/?secret=MFRGGZDF&counter=ABCD",
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range urlList {
|
||||||
|
if _, _, err := FromURL(urlList[i]); err == nil {
|
||||||
|
fmt.Println("hotp: URL should not have parsed successfully")
|
||||||
|
fmt.Printf("\turl was: %s\n", urlList[i])
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user