From 87aa5234c15230800256f4d36b4a16b696014089 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Wed, 27 Mar 2019 11:37:05 -0700 Subject: [PATCH 1/4] Separate OTP generation into a library. No functional changes are intended; the main package now imports the library and uses it, but the implementation is unchanged. Specific highlights: - Change the names of the functions to avert stutter following the advice of Effective Go: https://golang.org/doc/effective_go.html#package-names - Reorganize the helpers in main so control flow is easier to follow. - Add documentation comments. --- gauth.go | 79 +++++++++++++++----------------------------------- gauth/gauth.go | 48 ++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 55 deletions(-) create mode 100644 gauth/gauth.go diff --git a/gauth.go b/gauth.go index 1e7d75d..309e771 100644 --- a/gauth.go +++ b/gauth.go @@ -4,72 +4,20 @@ import ( "bytes" "crypto/aes" "crypto/cipher" - "crypto/hmac" - "crypto/sha1" "crypto/sha256" - "encoding/base32" "encoding/csv" "fmt" "io/ioutil" "log" - "math/big" "os/user" "path" "strings" "syscall" - "time" + "github.com/pcarrier/gauth/gauth" "golang.org/x/crypto/ssh/terminal" ) -func TimeStamp() (int64, int) { - time := time.Now().Unix() - return time / 30, int(time % 30) -} - -func normalizeSecret(sec string) string { - noPadding := strings.ToUpper(strings.Replace(sec, " ", "", -1)) - padLength := 8 - (len(noPadding) % 8) - if padLength < 8 { - return noPadding + strings.Repeat("=", padLength) - } - return noPadding -} - -func AuthCode(sec string, ts int64) (string, error) { - key, err := base32.StdEncoding.DecodeString(sec) - if err != nil { - return "", err - } - enc := hmac.New(sha1.New, key) - msg := make([]byte, 8, 8) - msg[0] = (byte)(ts >> (7 * 8) & 0xff) - msg[1] = (byte)(ts >> (6 * 8) & 0xff) - msg[2] = (byte)(ts >> (5 * 8) & 0xff) - msg[3] = (byte)(ts >> (4 * 8) & 0xff) - msg[4] = (byte)(ts >> (3 * 8) & 0xff) - msg[5] = (byte)(ts >> (2 * 8) & 0xff) - msg[6] = (byte)(ts >> (1 * 8) & 0xff) - msg[7] = (byte)(ts >> (0 * 8) & 0xff) - if _, err := enc.Write(msg); err != nil { - return "", err - } - hash := enc.Sum(nil) - offset := hash[19] & 0x0f - trunc := hash[offset : offset+4] - trunc[0] &= 0x7F - res := new(big.Int).Mod(new(big.Int).SetBytes(trunc), big.NewInt(1000000)) - return fmt.Sprintf("%06d", res), nil -} - -func authCodeOrDie(sec string, ts int64) string { - str, e := AuthCode(sec, ts) - if e != nil { - log.Fatal(e) - } - return str -} - func main() { user, e := user.Current() if e != nil { @@ -83,7 +31,7 @@ func main() { } // Support for 'openssl enc -aes-128-cbc -md sha256 -pass pass:' - if bytes.Compare(cfgContent[:8], []byte{0x53, 0x61, 0x6c, 0x74, 0x65, 0x64, 0x5f, 0x5f}) == 0 { + if bytes.HasPrefix(cfgContent, []byte("Salted__")) { fmt.Printf("Encryption password: ") passwd, e := terminal.ReadPassword(int(syscall.Stdin)) fmt.Printf("\n") @@ -122,7 +70,7 @@ func main() { log.Fatal(e) } - currentTS, progress := TimeStamp() + currentTS, progress := gauth.IndexNow() prevTS := currentTS - 1 nextTS := currentTS + 1 @@ -150,3 +98,24 @@ func main() { func leftPad(s string, padStr string, pLen int) string { return strings.Repeat(padStr, pLen) + s } + +// normalizeSecret cleans up whitespace and adds any missing padding to sec to +// use it as an OTP seed. +func normalizeSecret(sec string) string { + noPadding := strings.ToUpper(strings.Replace(sec, " ", "", -1)) + padLength := 8 - (len(noPadding) % 8) + if padLength < 8 { + return noPadding + strings.Repeat("=", padLength) + } + return noPadding +} + +// authCodeOrDie returns a code for the specified parameters, or aborts if an +// error occurred while generating the code. +func authCodeOrDie(sec string, ts int64) string { + str, e := gauth.Code(sec, ts) + if e != nil { + log.Fatal(e) + } + return str +} diff --git a/gauth/gauth.go b/gauth/gauth.go new file mode 100644 index 0000000..5976ec8 --- /dev/null +++ b/gauth/gauth.go @@ -0,0 +1,48 @@ +// Package gauth implements the time-based OTP generation scheme used by Google +// Authenticator. +package gauth + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base32" + "fmt" + "math/big" + "time" +) + +// IndexNow returns the current 30-second time slice index, and the number of +// seconds remaining until it ends. +func IndexNow() (int64, int) { + time := time.Now().Unix() + return time / 30, int(time % 30) +} + +// Code returns the OTP code for the given secret at the specified time slice +// index. It will report an error if the secret is not valid Base32 or if HMAC +// generation fails. +func Code(sec string, ts int64) (string, error) { + key, err := base32.StdEncoding.DecodeString(sec) + if err != nil { + return "", err + } + enc := hmac.New(sha1.New, key) + msg := make([]byte, 8) + msg[0] = (byte)(ts >> (7 * 8) & 0xff) + msg[1] = (byte)(ts >> (6 * 8) & 0xff) + msg[2] = (byte)(ts >> (5 * 8) & 0xff) + msg[3] = (byte)(ts >> (4 * 8) & 0xff) + msg[4] = (byte)(ts >> (3 * 8) & 0xff) + msg[5] = (byte)(ts >> (2 * 8) & 0xff) + msg[6] = (byte)(ts >> (1 * 8) & 0xff) + msg[7] = (byte)(ts >> (0 * 8) & 0xff) + if _, err := enc.Write(msg); err != nil { + return "", err + } + hash := enc.Sum(nil) + offset := hash[19] & 0x0f + trunc := hash[offset : offset+4] + trunc[0] &= 0x7F + res := new(big.Int).Mod(new(big.Int).SetBytes(trunc), big.NewInt(1000000)) + return fmt.Sprintf("%06d", res), nil +} From bf811361487a38156288e05794b90d615171e5d6 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Wed, 27 Mar 2019 11:53:52 -0700 Subject: [PATCH 2/4] Add basic unit tests for the gauth package. --- gauth/gauth_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 gauth/gauth_test.go diff --git a/gauth/gauth_test.go b/gauth/gauth_test.go new file mode 100644 index 0000000..4bed176 --- /dev/null +++ b/gauth/gauth_test.go @@ -0,0 +1,30 @@ +package gauth_test + +import ( + "testing" + + "github.com/pcarrier/gauth/gauth" +) + +func TestCode(t *testing.T) { + tests := []struct { + secret string + index int64 + want string + fail bool + }{ + // Manually verified with the Google authenticator app. + {"ABCDEFGH", 51790421, "305441", false}, + + // Invalid Base32 input for the secret. + {"blargh!", 123, "", true}, + } + for _, test := range tests { + got, err := gauth.Code(test.secret, test.index) + if err != nil && !test.fail { + t.Errorf("Code(%q, %d): unexpected error: %v", test.secret, test.index, err) + } else if got != test.want { + t.Errorf("Code(%q, %d): got %q, want %q", test.secret, test.index, got, test.want) + } + } +} From 0d0c6c886e6fdd747d26967bf3f7fd83e2ef55fd Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Wed, 27 Mar 2019 11:37:23 -0700 Subject: [PATCH 3/4] Add go.mod and go.sum files to support Go modules. --- go.mod | 8 ++++++++ go.sum | 10 ++++++++++ 2 files changed, 18 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..35caacb --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/pcarrier/gauth + +go 1.12 + +require ( + golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 + golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..808e7ec --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 h1:T5DasATyLQfmbTpfEXx/IOL9vfjzW6up+ZDkmHvIf2s= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From e30055f6750128e9d69d655867c0b6fe48f634f1 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Sat, 1 Jun 2019 16:59:38 -0700 Subject: [PATCH 4/4] Add recent versions of Go to the .travis.yml. --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fb4fbaf..887b4e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,7 @@ language: go go: - 1.7 - 1.8 - - 1.9 \ No newline at end of file + - 1.9 + - "1.10" + - 1.11 + - 1.12