From f9a3a4b3949dddecae413b97904c895b2cd887bf Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 30 Mar 2016 12:49:29 -0400 Subject: Add MFA functionality --- .../src/github.com/dgryski/dgoogauth/.travis.yml | 1 + .../src/github.com/dgryski/dgoogauth/README.md | 15 ++ .../src/github.com/dgryski/dgoogauth/googauth.go | 199 ++++++++++++++++ .../github.com/dgryski/dgoogauth/googauth_test.go | 251 +++++++++++++++++++++ 4 files changed, 466 insertions(+) create mode 100644 Godeps/_workspace/src/github.com/dgryski/dgoogauth/.travis.yml create mode 100644 Godeps/_workspace/src/github.com/dgryski/dgoogauth/README.md create mode 100644 Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth.go create mode 100644 Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth_test.go (limited to 'Godeps/_workspace/src/github.com/dgryski/dgoogauth') diff --git a/Godeps/_workspace/src/github.com/dgryski/dgoogauth/.travis.yml b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/.travis.yml new file mode 100644 index 000000000..4f2ee4d97 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/.travis.yml @@ -0,0 +1 @@ +language: go diff --git a/Godeps/_workspace/src/github.com/dgryski/dgoogauth/README.md b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/README.md new file mode 100644 index 000000000..75fdde78a --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/README.md @@ -0,0 +1,15 @@ +This is a Go implementation of the Google Authenticator library. + +[![Build Status](https://travis-ci.org/dgryski/dgoogauth.png)](https://travis-ci.org/dgryski/dgoogauth) + +Copyright (c) 2012 Damian Gryski +This code is licensed under the Apache License, version 2.0 + +It implements the one-time-password algorithms specified in: + +* RFC 4226 (HOTP: An HMAC-Based One-Time Password Algorithm) +* RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) + +You can learn more about the Google Authenticator library at its project page: + +* https://github.com/google/google-authenticator diff --git a/Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth.go b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth.go new file mode 100644 index 000000000..1efddcc20 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth.go @@ -0,0 +1,199 @@ +/* +Package dgoogauth implements the one-time password algorithms supported by Google Authenticator + +This package supports the HMAC-Based One-time Password (HOTP) algorithm +specified in RFC 4226 and the Time-based One-time Password (TOTP) algorithm +specified in RFC 6238. +*/ +package dgoogauth + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base32" + "encoding/binary" + "errors" + "net/url" + "sort" + "strconv" + "time" +) + +// Much of this code assumes int == int64, which probably is not the case. + +// ComputeCode computes the response code for a 64-bit challenge 'value' using the secret 'secret'. +// To avoid breaking compatibility with the previous API, it returns an invalid code (-1) when an error occurs, +// but does not silently ignore them (it forces a mismatch so the code will be rejected). +func ComputeCode(secret string, value int64) int { + + key, err := base32.StdEncoding.DecodeString(secret) + if err != nil { + return -1 + } + + hash := hmac.New(sha1.New, key) + err = binary.Write(hash, binary.BigEndian, value) + if err != nil { + return -1 + } + h := hash.Sum(nil) + + offset := h[19] & 0x0f + + truncated := binary.BigEndian.Uint32(h[offset : offset+4]) + + truncated &= 0x7fffffff + code := truncated % 1000000 + + return int(code) +} + +// ErrInvalidCode indicate the supplied one-time code was not valid +var ErrInvalidCode = errors.New("invalid code") + +// OTPConfig is a one-time-password configuration. This object will be modified by calls to +// Authenticate and should be saved to ensure the codes are in fact only used +// once. +type OTPConfig struct { + Secret string // 80-bit base32 encoded string of the user's secret + WindowSize int // valid range: technically 0..100 or so, but beyond 3-5 is probably bad security + HotpCounter int // the current otp counter. 0 if the user uses time-based codes instead. + DisallowReuse []int // timestamps in the current window unavailable for re-use + ScratchCodes []int // an array of 8-digit numeric codes that can be used to log in + UTC bool // use UTC for the timestamp instead of local time +} + +func (c *OTPConfig) checkScratchCodes(code int) bool { + + for i, v := range c.ScratchCodes { + if code == v { + // remove this code from the list of valid ones + l := len(c.ScratchCodes) - 1 + c.ScratchCodes[i] = c.ScratchCodes[l] // copy last element over this element + c.ScratchCodes = c.ScratchCodes[0:l] // and trim the list length by 1 + return true + } + } + + return false +} + +func (c *OTPConfig) checkHotpCode(code int) bool { + + for i := 0; i < c.WindowSize; i++ { + if ComputeCode(c.Secret, int64(c.HotpCounter+i)) == code { + c.HotpCounter += i + 1 + // We don't check for overflow here, which means you can only authenticate 2^63 times + // After that, the counter is negative and the above 'if' test will fail. + // This matches the behaviour of the PAM module. + return true + } + } + + // we must always advance the counter if we tried to authenticate with it + c.HotpCounter++ + return false +} + +func (c *OTPConfig) checkTotpCode(t0, code int) bool { + + minT := t0 - (c.WindowSize / 2) + maxT := t0 + (c.WindowSize / 2) + for t := minT; t <= maxT; t++ { + if ComputeCode(c.Secret, int64(t)) == code { + + if c.DisallowReuse != nil { + for _, timeCode := range c.DisallowReuse { + if timeCode == t { + return false + } + } + + // code hasn't been used before + c.DisallowReuse = append(c.DisallowReuse, t) + + // remove all time codes outside of the valid window + sort.Ints(c.DisallowReuse) + min := 0 + for c.DisallowReuse[min] < minT { + min++ + } + // FIXME: check we don't have an off-by-one error here + c.DisallowReuse = c.DisallowReuse[min:] + } + + return true + } + } + + return false +} + +// Authenticate a one-time-password against the given OTPConfig +// Returns true/false if the authentication was successful. +// Returns error if the password is incorrectly formatted (not a zero-padded 6 or non-zero-padded 8 digit number). +func (c *OTPConfig) Authenticate(password string) (bool, error) { + + var scratch bool + + switch { + case len(password) == 6 && password[0] >= '0' && password[0] <= '9': + break + case len(password) == 8 && password[0] >= '1' && password[0] <= '9': + scratch = true + break + default: + return false, ErrInvalidCode + } + + code, err := strconv.Atoi(password) + + if err != nil { + return false, ErrInvalidCode + } + + if scratch { + return c.checkScratchCodes(code), nil + } + + // we have a counter value we can use + if c.HotpCounter > 0 { + return c.checkHotpCode(code), nil + } + + var t0 int + // assume we're on Time-based OTP + if c.UTC { + t0 = int(time.Now().UTC().Unix() / 30) + } else { + t0 = int(time.Now().Unix() / 30) + } + return c.checkTotpCode(t0, code), nil +} + +// ProvisionURI generates a URI that can be turned into a QR code to configure +// a Google Authenticator mobile app. +func (c *OTPConfig) ProvisionURI(user string) string { + return c.ProvisionURIWithIssuer(user, "") +} + +// ProvisionURIWithIssuer generates a URI that can be turned into a QR code +// to configure a Google Authenticator mobile app. It respects the recommendations +// on how to avoid conflicting accounts. +// +// See https://code.google.com/p/google-authenticator/wiki/ConflictingAccounts +func (c *OTPConfig) ProvisionURIWithIssuer(user string, issuer string) string { + auth := "totp/" + q := make(url.Values) + if c.HotpCounter > 0 { + auth = "hotp/" + q.Add("counter", strconv.Itoa(c.HotpCounter)) + } + q.Add("secret", c.Secret) + if issuer != "" { + q.Add("issuer", issuer) + auth += issuer + ":" + } + + return "otpauth://" + auth + user + "?" + q.Encode() +} diff --git a/Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth_test.go b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth_test.go new file mode 100644 index 000000000..031922c47 --- /dev/null +++ b/Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth_test.go @@ -0,0 +1,251 @@ +package dgoogauth + +import ( + "strconv" + "testing" + "time" +) + +// Test vectors via: +// http://code.google.com/p/google-authenticator/source/browse/libpam/pam_google_authenticator_unittest.c +// https://google-authenticator.googlecode.com/hg/libpam/totp.html + +var codeTests = []struct { + secret string + value int64 + code int +}{ + {"2SH3V3GDW7ZNMGYE", 1, 293240}, + {"2SH3V3GDW7ZNMGYE", 5, 932068}, + {"2SH3V3GDW7ZNMGYE", 10000, 50548}, +} + +func TestCode(t *testing.T) { + + for _, v := range codeTests { + c := ComputeCode(v.secret, v.value) + + if c != v.code { + t.Errorf("computeCode(%s, %d): got %d expected %d\n", v.secret, v.value, c, v.code) + } + + } +} + +func TestScratchCode(t *testing.T) { + + var cotp OTPConfig + + cotp.ScratchCodes = []int{11112222, 22223333} + + var scratchTests = []struct { + code int + result bool + }{ + {33334444, false}, + {11112222, true}, + {11112222, false}, + {22223333, true}, + {22223333, false}, + {33334444, false}, + } + + for _, s := range scratchTests { + r := cotp.checkScratchCodes(s.code) + if r != s.result { + t.Errorf("scratchcode(%d) failed: got %t expected %t", s.code, r, s.result) + } + } +} + +func TestHotpCode(t *testing.T) { + + var cotp OTPConfig + + // reuse our test values from above + // perhaps create more? + cotp.Secret = "2SH3V3GDW7ZNMGYE" + cotp.HotpCounter = 1 + cotp.WindowSize = 3 + + var counterCodes = []struct { + code int + result bool + counter int + }{ + { /* 1 */ 293240, true, 2}, // increments on success + { /* 1 */ 293240, false, 3}, // and failure + { /* 5 */ 932068, true, 6}, // inside of window + { /* 10 */ 481725, false, 7}, // outside of window + { /* 10 */ 481725, false, 8}, // outside of window + { /* 10 */ 481725, true, 11}, // now inside of window + } + + for i, s := range counterCodes { + r := cotp.checkHotpCode(s.code) + if r != s.result { + t.Errorf("counterCode(%d) (step %d) failed: got %t expected %t", s.code, i, r, s.result) + } + if cotp.HotpCounter != s.counter { + t.Errorf("hotpCounter incremented poorly: got %d expected %d", cotp.HotpCounter, s.counter) + } + } +} + +func TestTotpCode(t *testing.T) { + + var cotp OTPConfig + + // reuse our test values from above + cotp.Secret = "2SH3V3GDW7ZNMGYE" + cotp.WindowSize = 5 + + var windowTest = []struct { + code int + t0 int + result bool + }{ + {50548, 9997, false}, + {50548, 9998, true}, + {50548, 9999, true}, + {50548, 10000, true}, + {50548, 10001, true}, + {50548, 10002, true}, + {50548, 10003, false}, + } + + for i, s := range windowTest { + r := cotp.checkTotpCode(s.t0, s.code) + if r != s.result { + t.Errorf("counterCode(%d) (step %d) failed: got %t expected %t", s.code, i, r, s.result) + } + } + + cotp.DisallowReuse = make([]int, 0) + var noreuseTest = []struct { + code int + t0 int + result bool + disallowed []int + }{ + {50548 /* 10000 */, 9997, false, []int{}}, + {50548 /* 10000 */, 9998, true, []int{10000}}, + {50548 /* 10000 */, 9999, false, []int{10000}}, + {478726 /* 10001 */, 10001, true, []int{10000, 10001}}, + {646986 /* 10002 */, 10002, true, []int{10000, 10001, 10002}}, + {842639 /* 10003 */, 10003, true, []int{10001, 10002, 10003}}, + } + + for i, s := range noreuseTest { + r := cotp.checkTotpCode(s.t0, s.code) + if r != s.result { + t.Errorf("timeCode(%d) (step %d) failed: got %t expected %t", s.code, i, r, s.result) + } + if len(cotp.DisallowReuse) != len(s.disallowed) { + t.Errorf("timeCode(%d) (step %d) failed: disallowReuse len mismatch: got %d expected %d", s.code, i, len(cotp.DisallowReuse), len(s.disallowed)) + } else { + same := true + for j := range s.disallowed { + if s.disallowed[j] != cotp.DisallowReuse[j] { + same = false + } + } + if !same { + t.Errorf("timeCode(%d) (step %d) failed: disallowReused: got %v expected %v", s.code, i, cotp.DisallowReuse, s.disallowed) + } + } + } +} + +func TestAuthenticate(t *testing.T) { + + otpconf := &OTPConfig{ + Secret: "2SH3V3GDW7ZNMGYE", + WindowSize: 3, + HotpCounter: 1, + ScratchCodes: []int{11112222, 22223333}, + } + + type attempt struct { + code string + result bool + } + + var attempts = []attempt{ + {"foobar", false}, // not digits + {"1fooba", false}, // not valid number + {"1111111", false}, // bad length + { /* 1 */ "293240", true}, // hopt increments on success + { /* 1 */ "293240", false}, // hopt failure + {"33334444", false}, // scratch + {"11112222", true}, + {"11112222", false}, + } + + for _, a := range attempts { + r, _ := otpconf.Authenticate(a.code) + if r != a.result { + t.Errorf("bad result from code=%s: got %t expected %t\n", a.code, r, a.result) + } + } + + // let's check some time-based codes + otpconf.HotpCounter = 0 + // I haven't mocked the clock, so we'll just compute one + var t0 int64 + if otpconf.UTC { + t0 = int64(time.Now().UTC().Unix() / 30) + } else { + t0 = int64(time.Now().Unix() / 30) + } + c := ComputeCode(otpconf.Secret, t0) + + invalid := c + 1 + attempts = []attempt{ + {strconv.Itoa(invalid), false}, + {strconv.Itoa(c), true}, + } + + for _, a := range attempts { + r, _ := otpconf.Authenticate(a.code) + if r != a.result { + t.Errorf("bad result from code=%s: got %t expected %t\n", a.code, r, a.result) + } + + otpconf.UTC = true + r, _ = otpconf.Authenticate(a.code) + if r != a.result { + t.Errorf("bad result from code=%s: got %t expected %t\n", a.code, r, a.result) + } + otpconf.UTC = false + } + +} + +func TestProvisionURI(t *testing.T) { + otpconf := OTPConfig{ + Secret: "x", + } + + cases := []struct { + user, iss string + hotp bool + out string + }{ + {"test", "", false, "otpauth://totp/test?secret=x"}, + {"test", "", true, "otpauth://hotp/test?counter=1&secret=x"}, + {"test", "Company", true, "otpauth://hotp/Company:test?counter=1&issuer=Company&secret=x"}, + {"test", "Company", false, "otpauth://totp/Company:test?issuer=Company&secret=x"}, + } + + for i, c := range cases { + otpconf.HotpCounter = 0 + if c.hotp { + otpconf.HotpCounter = 1 + } + got := otpconf.ProvisionURIWithIssuer(c.user, c.iss) + if got != c.out { + t.Errorf("%d: want %q, got %q", i, c.out, got) + } + } +} -- cgit v1.2.3-1-g7c22