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:
parent
9083b3c311
commit
c57414b83b
@ -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
|
||||
|
66
gauth.go
66
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))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
1
gauth/testdata/encrypted.csv
vendored
Normal file
@ -0,0 +1 @@
|
||||
Salted__$1ê:Ã'”£ÐmµÄì…ãEÖ2:ÞúÞ^<5E><1D>%Õ‹YŸùL*‘àS:ö—=ø<>KÈÛž
|
|
3
gauth/testdata/plaintext.csv
vendored
Normal file
3
gauth/testdata/plaintext.csv
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
test2:AEBAGBAFAYDQQCIK
|
||||
test1:AAAQEAYEAUDAOCAJ
|
||||
|
|
6
go.mod
6
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
|
||||
)
|
||||
|
12
go.sum
12
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=
|
||||
|
Loading…
x
Reference in New Issue
Block a user