From 0982f47ce3577ef874ef9f8cc0a51217976ca987 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Fri, 20 Dec 2013 17:00:01 -0700 Subject: [PATCH] Add last night's progress. Basic functionality for HOTP, TOTP, and YubiKey OTP. Still need YubiKey HMAC, serialisation, check, and scan. --- hotp.go | 10 +-- modhex/example_test.go | 29 ++++++++ modhex/modhex.go | 112 ++++++++++++++++++++++++++++ modhex/modhex_test.go | 163 +++++++++++++++++++++++++++++++++++++++++ oath.go | 20 ++--- otp.go | 13 ++-- otp_test.go | 2 +- totp.go | 10 +-- yubikey.go | 56 ++++++++++++++ 9 files changed, 386 insertions(+), 29 deletions(-) create mode 100644 modhex/example_test.go create mode 100644 modhex/modhex.go create mode 100644 modhex/modhex_test.go create mode 100644 yubikey.go diff --git a/hotp.go b/hotp.go index a643338..32c8d2f 100644 --- a/hotp.go +++ b/hotp.go @@ -10,7 +10,7 @@ import ( ) type HOTP struct { - *oath + *OATH } func (otp *HOTP) Type() Type { @@ -19,7 +19,7 @@ func (otp *HOTP) Type() Type { func NewHOTP(key []byte, counter uint64, digits int) *HOTP { return &HOTP{ - oath: &oath{ + OATH: &OATH{ key: key, counter: counter, size: digits, @@ -30,13 +30,13 @@ func NewHOTP(key []byte, counter uint64, digits int) *HOTP { } func (otp *HOTP) OTP() string { - code := otp.oath.OTP(otp.counter) + code := otp.OATH.OTP(otp.counter) otp.counter++ return code } func (otp *HOTP) URL(label string) string { - return otp.oath.URL(otp.Type(), label) + return otp.OATH.URL(otp.Type(), label) } func (otp *HOTP) SetProvider(provider string) { @@ -87,5 +87,5 @@ func hotpFromURL(u *url.URL) (*HOTP, string, error) { } func (otp *HOTP) QR(label string) ([]byte, error) { - return otp.oath.QR(otp.Type(), label) + return otp.OATH.QR(otp.Type(), label) } diff --git a/modhex/example_test.go b/modhex/example_test.go new file mode 100644 index 0000000..367c2ad --- /dev/null +++ b/modhex/example_test.go @@ -0,0 +1,29 @@ +package modhex + +import ( + "fmt" + "github.com/gokyle/twofactor/modhex" +) + +var out = "fjhghrhrhvdrdciihvidhrhfdb" +var in = "Hello, world!" + +func ExampleEncoding_EncodeToString() { + data := []byte("Hello, world!") + str := modhex.StdEncoding.EncodeToString(data) + fmt.Println(str) + // Output: + // fjhghrhrhvdrdciihvidhrhfdb +} + +func ExampleEncoding_DecodeString() { + str := "fjhghrhrhvdrdciihvidhrhfdb" + data, err := modhex.StdEncoding.DecodeString(str) + if err != nil { + fmt.Printf("%v\n", err) + return + } + fmt.Printf("%s", string(data)) + // Output: + // Hello, world! +} diff --git a/modhex/modhex.go b/modhex/modhex.go new file mode 100644 index 0000000..94d34a9 --- /dev/null +++ b/modhex/modhex.go @@ -0,0 +1,112 @@ +// Package modhex implements the modified hexadecimal encoding as used +// by Yubico in their series of products. +package modhex + +import "fmt" + +// Encoding is a mapping of hexadecimal values to a new byte value. +// This means that the encoding for a single byte is two bytes. +type Encoding struct { + decoding map[byte]byte + encoding [16]byte +} + +// A CorruptInputError is returned if the input string contains +// invalid characters for the encoding or if the input is the wrong +// length. It contains the number of bytes written out. +type CorruptInputError struct { + written int64 +} + +func (err CorruptInputError) Error() string { + return fmt.Sprintf("modhex: corrupt input at byte %d", err.written) +} + +func (err CorruptInputError) Written() int64 { + return err.written +} + +var encodeStd = "cbdefghijklnrtuv" + +// NewEncoding builds a new encoder from the alphabet passed in, +// which must be a 16-byte string. +func NewEncoding(encoder string) *Encoding { + if len(encoder) != 16 { + return nil + } + + enc := new(Encoding) + enc.decoding = make(map[byte]byte) + for i := range encoder { + enc.encoding[i] = encoder[i] + enc.decoding[encoder[i]] = byte(i) + } + return enc +} + +// StdEncoding is the canonical modhex alphabet as used by Yubico. +var StdEncoding = NewEncoding(encodeStd) + +// Encode encodes src to dst, writing at most EncodedLen(len(src)) +// bytes to dst. +func (enc *Encoding) Encode(dst, src []byte) { + out := dst + + for i := 0; i < len(src) && len(out) > 1; i++ { + var b [2]byte + b[0] = enc.encoding[(src[i]&0xf0)>>4] + b[1] = enc.encoding[src[i]&0xf] + copy(out[:2], b[:]) + out = out[2:] + } +} + +// EncodedLen returns the encoded length of a buffer of n bytes. +func EncodedLen(n int) int { + return n << 1 +} + +// DecodedLen returns the decoded length of a buffer of n bytes. +func DecodedLen(n int) int { + return n >> 1 +} + +// Decode decodes src into dst, which will be at most DecodedLen(len(src)). +// It returns the number of bytes written, and any error that occurred. +func (enc *Encoding) Decode(dst, src []byte) (n int, err error) { + out := dst + + for i := 0; i < len(src); i += 2 { + if (len(src) - i) < 2 { + return i >> 1, CorruptInputError{int64(i >> 1)} + } + var b byte + if high, ok := enc.decoding[src[i]]; !ok { + return i >> 1, CorruptInputError{int64(i >> 1)} + } else if low, ok := enc.decoding[src[i+1]]; !ok { + return i >> 1, CorruptInputError{int64(i >> 1)} + } else { + b = high << 4 + b += low + out[0] = b + out = out[1:] + } + } + return len(dst), nil +} + +// EncodeToString is a convenience function to encode src as a +// string. +func (enc *Encoding) EncodeToString(src []byte) string { + dst := make([]byte, EncodedLen(len(src))) + enc.Encode(dst, src) + return string(dst) +} + +// DecodeString decodes the string passed in as its decoded bytes. +func (enc *Encoding) DecodeString(s string) ([]byte, error) { + dst := make([]byte, DecodedLen(len(s))) + src := []byte(s) + _, err := enc.Decode(dst, src) + return dst, err +} diff --git a/modhex/modhex_test.go b/modhex/modhex_test.go new file mode 100644 index 0000000..7f7212e --- /dev/null +++ b/modhex/modhex_test.go @@ -0,0 +1,163 @@ +package modhex + +import "bytes" +import "fmt" +import "testing" + +func TestInvalidEncoder(t *testing.T) { + s := "" + for i := 0; i < 16; i++ { + if NewEncoding(s) != nil { + fmt.Println("modhex: NewEncoding accepted bad encoding") + t.FailNow() + } + } +} + +var encodeTests = []struct { + In []byte + Out []byte +}{ + {[]byte{0x47}, []byte("fi")}, + {[]byte{0xba, 0xad, 0xf0, 0x0d}, []byte("nlltvcct")}, +} + +var decodeFail = [][]byte{ + []byte{0x47}, + []byte("abcdef"), +} + +func TestStdEncodingEncode(t *testing.T) { + enc := StdEncoding + for _, et := range encodeTests { + out := make([]byte, EncodedLen(len(et.In))) + enc.Encode(out, et.In) + if !bytes.Equal(out, et.Out) { + fmt.Println("modhex: StdEncoding: bad encoding") + fmt.Printf("\texpected: %x\n", et.Out) + fmt.Printf("\t actual: %x\n", out) + t.FailNow() + } + } +} + +func TestStdDecoding(t *testing.T) { + enc := StdEncoding + for _, et := range encodeTests { + out := make([]byte, DecodedLen(len(et.Out))) + n, err := enc.Decode(out, et.Out) + if err != nil { + fmt.Printf("%v\n", err) + t.FailNow() + } else if n != len(et.In) { + fmt.Println("modhex: bad decoded length") + t.FailNow() + } else if !bytes.Equal(out, et.In) { + fmt.Println("modhex: StdEncoding: bad decoding") + fmt.Printf("\texpected: %x\n", et.In) + fmt.Printf("\t actual: %x\n", out) + t.FailNow() + } + } +} + +func TestStdDecodingFail(t *testing.T) { + enc := StdEncoding + for _, et := range decodeFail { + dst := make([]byte, DecodedLen(len(et))) + _, err := enc.Decode(dst, et) + if err == nil { + fmt.Println("modhex: decode should fail") + t.FailNow() + } + } +} + +func TestStdEncodingToString(t *testing.T) { + enc := StdEncoding + for _, et := range encodeTests { + out := enc.EncodeToString(et.In) + if out != string(et.Out) { + fmt.Println("modhex: StdEncoding: bad encoding") + fmt.Printf("\texpected: %x\n", et.Out) + fmt.Printf("\t actual: %x\n", out) + t.FailNow() + } + } +} + +func TestStdEncodingString(t *testing.T) { + enc := StdEncoding + for _, et := range encodeTests { + out, err := enc.DecodeString(string(et.Out)) + if err != nil { + fmt.Printf("%v\n", err) + t.FailNow() + } else if !bytes.Equal(out, et.In) { + fmt.Println("modhex: StdEncoding: bad encoding") + fmt.Printf("\texpected: %x\n", et.In) + fmt.Printf("\t actual: %x\n", out) + t.FailNow() + } + } +} + +var corruptTests = []struct { + In []byte + Written int64 + Error string +}{ + {[]byte("aa"), 0, "modhex: corrupt input at byte 0"}, + {[]byte("ca"), 0, "modhex: corrupt input at byte 0"}, + {[]byte("ccac"), 1, "modhex: corrupt input at byte 1"}, + {[]byte("ccca"), 1, "modhex: corrupt input at byte 1"}, +} + +func TestCorruptInputError(t *testing.T) { + enc := StdEncoding + for _, ct := range corruptTests { + dst := make([]byte, DecodedLen(len(ct.In))) + n, err := enc.Decode(dst, ct.In) + if err == nil { + fmt.Println("modhex: decode should fail") + t.FailNow() + } else if (err.(CorruptInputError)).Written() != ct.Written { + fmt.Printf("modhex: decode should fail at byte %d, failed at byte %d\n", + ct.Written, (err.(CorruptInputError)).Written()) + t.FailNow() + } else if err.Error() != ct.Error { + fmt.Printf("modhex: invalid error '%s' returned\n", err.Error()) + fmt.Printf(" (expected '%s')\n", ct.Error) + t.FailNow() + } else if int64(n) != ct.Written { + fmt.Printf("modhex: decode should fail at byte %d, failed at byte %d\n", + ct.Written, n) + t.FailNow() + } + + } +} + +func TestCorruptInputErrorString(t *testing.T) { + enc := StdEncoding + for _, ct := range corruptTests { + _, err := enc.DecodeString(string(ct.In)) + if err == nil { + fmt.Println("modhex: decode should fail") + t.FailNow() + } else if (err.(CorruptInputError)).Written() != ct.Written { + fmt.Printf("modhex: decode should fail at byte %d, failed at byte %d\n", + ct.Written, (err.(CorruptInputError)).Written()) + t.FailNow() + } else if err.Error() != ct.Error { + fmt.Printf("modhex: invalid error '%s' returned\n", err.Error()) + fmt.Printf(" (expected '%s')\n", ct.Error) + t.FailNow() + } + } +} + +func TestFoo(t *testing.T) { + fmt.Println("Hello, world!->", StdEncoding.EncodeToString([]byte("Hello, world!"))) + t.FailNow() +} diff --git a/oath.go b/oath.go index b50ebb3..9f1bf6a 100644 --- a/oath.go +++ b/oath.go @@ -13,8 +13,8 @@ import ( const defaultSize = 6 -// oath provides a baseline struct for the two OATH algorithms. -type oath struct { +// OATH provides a baseline structure for the two OATH algorithms. +type OATH struct { key []byte counter uint64 size int @@ -23,27 +23,27 @@ type oath struct { provider string } -func (o oath) Size() int { +func (o OATH) Size() int { return o.size } -func (o oath) Counter() uint64 { +func (o OATH) Counter() uint64 { return o.counter } -func (o oath) SetCounter(counter uint64) { +func (o OATH) SetCounter(counter uint64) { o.counter = counter } -func (o oath) Key() []byte { +func (o OATH) Key() []byte { return o.key[:] } -func (o oath) Hash() func() hash.Hash { +func (o OATH) Hash() func() hash.Hash { return o.hash } -func (o oath) URL(t Type, label string) string { +func (o OATH) URL(t Type, label string) string { secret := base32.StdEncoding.EncodeToString(o.key) u := url.URL{} v := url.Values{} @@ -96,7 +96,7 @@ var digits = []int64{ // 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 { +func (o OATH) OTP(counter uint64) string { var ctr [8]byte binary.BigEndian.PutUint64(ctr[:], counter) @@ -132,7 +132,7 @@ func truncate(in []byte) int64 { // 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) { +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 { diff --git a/otp.go b/otp.go index d8fcf28..dbc1fe1 100644 --- a/otp.go +++ b/otp.go @@ -13,6 +13,7 @@ type Type uint const ( OATH_HOTP = iota OATH_TOTP + YUBIKEY ) // PRNG is an io.Reader that provides a cryptographically secure @@ -47,24 +48,20 @@ type OTP interface { // 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 { +func otpString(otp OTP) string { var typeName string switch otp.Type() { case OATH_HOTP: typeName = "OATH-HOTP" case OATH_TOTP: typeName = "OATH-TOTP" + case YUBIKEY: + return fmt.Sprintf("YubiKey with %d byte public identity", + len(otp.(*YubiKey).Public())) } return fmt.Sprintf("%s, %d", typeName, otp.Size()) } diff --git a/otp_test.go b/otp_test.go index 57643cf..7fec809 100644 --- a/otp_test.go +++ b/otp_test.go @@ -6,7 +6,7 @@ import "testing" func TestHOTPString(t *testing.T) { hotp := NewHOTP(nil, 0, 6) - hotpString := OTPString(hotp) + hotpString := otpString(hotp) if hotpString != "OATH-HOTP, 6" { fmt.Println("twofactor: invalid OTP string") t.FailNow() diff --git a/totp.go b/totp.go index f475212..2727a41 100644 --- a/totp.go +++ b/totp.go @@ -14,7 +14,7 @@ import ( ) type TOTP struct { - *oath + *OATH step uint64 } @@ -23,7 +23,7 @@ func (otp *TOTP) Type() Type { } func (otp *TOTP) otp(counter uint64) string { - return otp.oath.OTP(counter) + return otp.OATH.OTP(counter) } func (otp *TOTP) OTP() string { @@ -31,7 +31,7 @@ func (otp *TOTP) OTP() string { } func (otp *TOTP) URL(label string) string { - return otp.oath.URL(otp.Type(), label) + return otp.OATH.URL(otp.Type(), label) } func (otp *TOTP) SetProvider(provider string) { @@ -53,7 +53,7 @@ func NewTOTP(key []byte, start uint64, step uint64, digits int, algo crypto.Hash } return &TOTP{ - oath: &oath{ + OATH: &OATH{ key: key, counter: start, size: digits, @@ -147,5 +147,5 @@ func totpFromURL(u *url.URL) (*TOTP, string, error) { } func (otp *TOTP) QR(label string) ([]byte, error) { - return otp.oath.QR(otp.Type(), label) + return otp.OATH.QR(otp.Type(), label) } diff --git a/yubikey.go b/yubikey.go new file mode 100644 index 0000000..6d68e1d --- /dev/null +++ b/yubikey.go @@ -0,0 +1,56 @@ +package twofactor + +// Implement YubiKey OTP and YubiKey HOTP. + +import ( + "github.com/conformal/yubikey" + "github.com/gokyle/twofactor/modhex" + "hash" +) + +// YubiKey is an implementation of the YubiKey hard token. Note +// that the internal counter only actually uses 32 bits. +type YubiKey struct { + token yubikey.Token + counter uint64 + key yubikey.Key + public []byte +} + +func (yk *YubiKey) Public() []byte { + return yk.public[:] +} + +func (yk *YubiKey) Counter() uint64 { + return yk.counter +} + +func (yk *YubiKey) SetCounter(counter uint64) { + yk.counter = counter & 0xffffffff +} + +func (yk *YubiKey) Key() []byte { + return yk.key[:] +} + +func (yk *YubiKey) Size() int { + return yubikey.OTPSize + len(yk.public) +} + +func (yk *YubiKey) OTP() string { + otp := yk.token.Generate(yk.key) + if otp == nil { + return "" + } + return modhex.StdEncoding.EncodeToString(otp.Bytes()) +} + +// Hash always returns nil, as the YubiKey tokens do not use a hash +// function. +func (yk *YubiKey) Hash() func() hash.Hash { + return nil +} + +func (yk *YubiKey) Type() Type { + return YUBIKEY +}