From c57414b83b103d71ab56ffe51b69d56b28a6578a Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" <michael.j.fromberger@gmail.com> Date: Mon, 26 Oct 2020 18:42:15 -0700 Subject: [PATCH] Fix padding removal when decrypting a config file. (#36) OpenSSL uses PKCS#5 padding, and the decryption code was not removing it correctly. In some cases, this causes the last line of the decrypted config to be mangled and produces invalid results. To support this: - Move config loading to gauth.LoadConfigFile. - Inject a hook to read the user's password. - Add unit tests that decryption doesn't corrupt the result. - Update module dependencies. - Update Go versions in CI, and fix some config-check warnings. --- .travis.yml | 8 ++--- gauth.go | 66 +++++++++++------------------------- gauth/gauth.go | 54 +++++++++++++++++++++++++++++ gauth/gauth_test.go | 42 +++++++++++++++++++++++ gauth/testdata/encrypted.csv | 1 + gauth/testdata/plaintext.csv | 3 ++ go.mod | 6 ++-- go.sum | 12 +++---- 8 files changed, 132 insertions(+), 60 deletions(-) create mode 100644 gauth/testdata/encrypted.csv create mode 100644 gauth/testdata/plaintext.csv diff --git a/.travis.yml b/.travis.yml index 887b4e4..09d5ef3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,7 @@ -sudo: false language: go go: - - 1.7 - - 1.8 - - 1.9 - - "1.10" - 1.11 - 1.12 + - 1.13 + - 1.14 + - 1.15 diff --git a/gauth.go b/gauth.go index 59df65f..8cbd13e 100644 --- a/gauth.go +++ b/gauth.go @@ -2,12 +2,8 @@ package main import ( "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/sha256" "encoding/csv" "fmt" - "io/ioutil" "log" "os" "os/user" @@ -21,55 +17,27 @@ import ( ) func main() { - user, e := user.Current() - if e != nil { - log.Fatal(e) - } - cfgPath := path.Join(user.HomeDir, ".config/gauth.csv") - - cfgContent, e := ioutil.ReadFile(cfgPath) - if e != nil { - log.Fatal(e) + cfgPath := os.Getenv("GAUTH_CONFIG") + if cfgPath == "" { + user, err := user.Current() + if err != nil { + log.Fatal(err) + } + cfgPath = path.Join(user.HomeDir, ".config/gauth.csv") } - // Support for 'openssl enc -aes-128-cbc -md sha256 -pass pass:' - if bytes.HasPrefix(cfgContent, []byte("Salted__")) { - fmt.Printf("Encryption password: ") - passwd, e := terminal.ReadPassword(int(syscall.Stdin)) - fmt.Printf("\n") - if e != nil { - log.Fatal(e) - } - salt := cfgContent[8:16] - rest := cfgContent[16:] - salting := sha256.New() - salting.Write([]byte(passwd)) - salting.Write(salt) - sum := salting.Sum(nil) - key := sum[:16] - iv := sum[16:] - block, e := aes.NewCipher(key) - if e != nil { - log.Fatal(e) - } - - mode := cipher.NewCBCDecrypter(block, iv) - mode.CryptBlocks(rest, rest) - // Remove padding - i := len(rest) - 1 - for rest[i] < 16 { - i-- - } - cfgContent = rest[:i] + cfgContent, err := gauth.LoadConfigFile(cfgPath, getPassword) + if err != nil { + log.Fatalf("Loading config: %v", err) } cfgReader := csv.NewReader(bytes.NewReader(cfgContent)) // Unix-style tabular cfgReader.Comma = ':' - cfg, e := cfgReader.ReadAll() - if e != nil { - log.Fatal(e) + cfg, err := cfgReader.ReadAll() + if err != nil { + log.Fatalf("Decoding CSV: %v", err) } currentTS, progress := gauth.IndexNow() @@ -80,10 +48,16 @@ func main() { name, secret := record[0], record[1] prev, curr, next, err := gauth.Codes(secret, currentTS) if err != nil { - log.Fatalf("Code: %v", err) + log.Fatalf("Generating codes: %v", err) } fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", name, prev, curr, next) } tw.Flush() fmt.Printf("[%-29s]\n", strings.Repeat("=", progress)) } + +func getPassword() ([]byte, error) { + fmt.Printf("Encryption password: ") + defer fmt.Println() + return terminal.ReadPassword(int(syscall.Stdin)) +} diff --git a/gauth/gauth.go b/gauth/gauth.go index 330790e..612df31 100644 --- a/gauth/gauth.go +++ b/gauth/gauth.go @@ -3,6 +3,13 @@ package gauth import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "errors" + "fmt" + "io/ioutil" "time" "github.com/creachadair/otp" @@ -28,3 +35,50 @@ func Codes(sec string, ts int64) (prev, curr, next string, _ error) { next = cfg.HOTP(uint64(ts + 1)) return } + +// LoadConfigFile reads and decrypts, if necessary, the CSV config at path. +// The getPass function is called to obtain a password if needed. +func LoadConfigFile(path string, getPass func() ([]byte, error)) ([]byte, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + if !bytes.HasPrefix(data, []byte("Salted__")) { + return data, nil // not encrypted + } + + // Support for 'openssl enc -aes-128-cbc -md sha256 -pass pass:' + passwd, err := getPass() + if err != nil { + return nil, fmt.Errorf("reading passphrase: %v", err) + } + + salt := data[8:16] + rest := data[16:] + salting := sha256.New() + salting.Write([]byte(passwd)) + salting.Write(salt) + sum := salting.Sum(nil) + key := sum[:16] + iv := sum[16:] + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("creating cipher: %v", err) + } + + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(rest, rest) + + // Remove CBC padding and verify that the key was valid. + pad := int(rest[len(rest)-1]) + if pad == 0 || pad > len(rest) { + return nil, errors.New("invalid decryption key") + } + for i := len(rest) - pad; i < len(rest); i++ { + if int(rest[i]) != pad { + return nil, errors.New("invalid block padding") + } + } + return rest[:len(rest)-int(pad)], nil +} diff --git a/gauth/gauth_test.go b/gauth/gauth_test.go index ffe343e..de93664 100644 --- a/gauth/gauth_test.go +++ b/gauth/gauth_test.go @@ -1,6 +1,7 @@ package gauth_test import ( + "bytes" "testing" "github.com/pcarrier/gauth/gauth" @@ -28,3 +29,44 @@ func TestCodes(t *testing.T) { } } } + +//go:generate openssl enc -aes-128-cbc -md sha256 -pass pass:x -in testdata/plaintext.csv -out testdata/encrypted.csv + +func TestLoadConfig(t *testing.T) { + + // To update test data, edit testdata/plaintext.csv as desired, + // then run go generate ./... + // If you change the passphrase, update getPass below. + // + // For this test, the contents don't actually matter. + + var calledGetPass bool + + getPass := func() ([]byte, error) { + calledGetPass = true + return []byte("x"), nil + } + + // Load the plaintext configuration file, and verify that we did not try to + // decrypt its content. + plain, err := gauth.LoadConfigFile("testdata/plaintext.csv", getPass) + if err != nil { + t.Fatalf("Loading plaintext config: %v", err) + } else if calledGetPass { + t.Error("Loading plaintext unexpectedly called getPass") + calledGetPass = false + } + + // Load the encrypted configuration file, and verify that we were able to + // decrypt it successfully. + enc, err := gauth.LoadConfigFile("testdata/encrypted.csv", getPass) + if err != nil { + t.Fatalf("Loading encrypted config: %v", err) + } else if !calledGetPass { + t.Error("Loading encrypted did not call getPass") + } + + if !bytes.Equal(plain, enc) { + t.Errorf("Decrypted not equal to plaintext:\ngot %+v\nwant %+v", enc, plain) + } +} diff --git a/gauth/testdata/encrypted.csv b/gauth/testdata/encrypted.csv new file mode 100644 index 0000000..0185d1b --- /dev/null +++ b/gauth/testdata/encrypted.csv @@ -0,0 +1 @@ +Salted__$1�:�'���m����E�2:���^��%ՋY��L*��S:��=��K�۞ \ No newline at end of file diff --git a/gauth/testdata/plaintext.csv b/gauth/testdata/plaintext.csv new file mode 100644 index 0000000..ec462d0 --- /dev/null +++ b/gauth/testdata/plaintext.csv @@ -0,0 +1,3 @@ +test2:AEBAGBAFAYDQQCIK +test1:AAAQEAYEAUDAOCAJ + diff --git a/go.mod b/go.mod index d714e1f..16136bc 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/pcarrier/gauth go 1.12 require ( - github.com/creachadair/otp v0.1.0 - golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 - golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 // indirect + github.com/creachadair/otp v0.1.1 + golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 + golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a // indirect ) diff --git a/go.sum b/go.sum index 9a68a85..17e2488 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,12 @@ -github.com/creachadair/otp v0.1.0 h1:JgsbBS3KYbZ7/HE4t5ylRIFRjtDrhzpUHwmpZc03INY= -github.com/creachadair/otp v0.1.0/go.mod h1:vPuEqgSogZ1vtpF8OeUg28ke/PK2FIo85GZHJz74d0M= +github.com/creachadair/otp v0.1.1 h1:SMeGZefF9eP+QjDGCRbW5a5mptIaP+HkMvzV+OhsukA= +github.com/creachadair/otp v0.1.1/go.mod h1:vPuEqgSogZ1vtpF8OeUg28ke/PK2FIo85GZHJz74d0M= 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/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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/sys v0.0.0-20201020230747-6e5568b54d1a h1:e3IU37lwO4aq3uoRKINC7JikojFmE5gO7xhfxs8VC34= +golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=