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.
This commit is contained in:
M. J. Fromberger 2020-10-26 18:42:15 -07:00 committed by GitHub
parent 9083b3c311
commit c57414b83b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 132 additions and 60 deletions

View File

@ -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

View File

@ -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))
}

View File

@ -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
}

View File

@ -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)
}
}

1
gauth/testdata/encrypted.csv vendored Normal file
View File

@ -0,0 +1 @@
Salted__$1ê:Ã'”£ ÐmµÄì…ãEÖ2:ÞúÞ^<5E><1D>YŸùL*‘àS:ö—=ø<>KÈÛž
1 Salted__$1ê:Ã'”£ ÐmµÄì…ãEÖ2:ÞúÞ^��%Õ‹YŸùL*‘àS:ö—=ø�KÈÛž

3
gauth/testdata/plaintext.csv vendored Normal file
View File

@ -0,0 +1,3 @@
test2:AEBAGBAFAYDQQCIK
test1:AAAQEAYEAUDAOCAJ
1 test2:AEBAGBAFAYDQQCIK
2 test1:AAAQEAYEAUDAOCAJ

6
go.mod
View File

@ -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
)

12
go.sum
View File

@ -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=