From f90c19d0f47925759692c7bc20adef49cc38e4dd Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 7 Jun 2019 02:03:15 -0700 Subject: [PATCH] Swapped AES-CBC for GCM for all symmetric encryption; bolstered TLS configs --- README.md | 8 +-- cli/commands/decrypt.go | 2 +- cli/commands/encrypt.go | 2 +- cli/commands/root.go | 1 + cli/repositories/config.go | 32 ++++----- cmd/server/server.go | 11 ++- credentials/commands/cbc_to_gcm.go | 94 +++++++++++++++++++++++++ credentials/commands/create.go | 4 +- credentials/commands/get.go | 4 +- credentials/repositories/grpc_client.go | 4 ++ crypto/gcm.go | 60 ++++++++++++++++ 11 files changed, 192 insertions(+), 30 deletions(-) create mode 100644 credentials/commands/cbc_to_gcm.go create mode 100644 crypto/gcm.go diff --git a/README.md b/README.md index 7dd59de..f69af9f 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ API using mutual TLS encryption, backed by Redis and Docker. It is also capable a semi-automated fashion locally and to GCP thanks to Docker. In addition to the service there is `spc` (**s**elf**p**ass **C**LI), which is a fully fledged *selfpass* client -capable of interacting with the whole selfpass API and creating AES-CBC encrypted credentials using -a *private key* and *master password*. All of which is done using mutual TLS and an AES-CBC +capable of interacting with the whole selfpass API and creating AES-GCM encrypted credentials using +a *private key* and *master password*. All of which is done using mutual TLS and an AES-GCM encrypted config. #### Service Roadmap @@ -31,8 +31,8 @@ encrypted config. | Support credentials CRUD via gRPC. | 80% | TODO: Update | | Support mutual TLS. | 100% | | | Support storage of certs, PK, and host in AES-CBC encrypted config. | 100% | | -| Support AES-CBC encryption of passes and OTP secrets, using MP and PK. | 100% | | -| Support AES-CBC encryption of local files, using MP and PK. | 100% | | +| Support AES-GCM encryption of passes and OTP secrets, using MP and PK. | 100% | | +| Support AES-GCM encryption of local files, using MP and PK. | 100% | | #### Unplanned Goals diff --git a/cli/commands/decrypt.go b/cli/commands/decrypt.go index 9e5d30d..ea2b3c5 100644 --- a/cli/commands/decrypt.go +++ b/cli/commands/decrypt.go @@ -40,7 +40,7 @@ the new file.`, passkey, err := crypto.CombinePasswordAndKey([]byte(masterpass), []byte(key)) check(err) - contents, err = crypto.CBCDecrypt(passkey, contents) + contents, err = crypto.GCMDecrypt(passkey, contents) check(err) check(ioutil.WriteFile(fileout, contents, 0600)) diff --git a/cli/commands/encrypt.go b/cli/commands/encrypt.go index 440a187..d6bda05 100644 --- a/cli/commands/encrypt.go +++ b/cli/commands/encrypt.go @@ -36,7 +36,7 @@ new file.`, passkey, err := crypto.CombinePasswordAndKey([]byte(masterpass), []byte(key)) check(err) - contents, err = crypto.CBCEncrypt(passkey, contents) + contents, err = crypto.GCMEncrypt(passkey, contents) check(err) check(ioutil.WriteFile(fileEnc, contents, 0600)) diff --git a/cli/commands/root.go b/cli/commands/root.go index b895108..d1d04f0 100644 --- a/cli/commands/root.go +++ b/cli/commands/root.go @@ -36,6 +36,7 @@ can interact with the entire Selfpass API.`, rootCmd.AddCommand(commands.MakeCreate(mgr, makeInitClient(mgr, clientInit))) rootCmd.AddCommand(commands.MakeGet(mgr, makeInitClient(mgr, clientInit))) rootCmd.AddCommand(commands.MakeDelete(makeInitClient(mgr, clientInit))) + rootCmd.AddCommand(commands.MakeCBCtoGCM(mgr, makeInitClient(mgr, clientInit))) check(rootCmd.Execute()) } diff --git a/cli/repositories/config.go b/cli/repositories/config.go index ef16dc4..7fc8455 100644 --- a/cli/repositories/config.go +++ b/cli/repositories/config.go @@ -51,7 +51,7 @@ func (mgr *ConfigManager) OpenConfig() (output string, v *viper.Viper, err error mgr.cfgFile = &cfg var contents []byte - var wasNotEncrypted bool + var cipherAuthFailed bool if _, err := os.Open(cfg); os.IsNotExist(err) { return output, mgr.v, fmt.Errorf("no config found, run 'init' command") @@ -62,22 +62,22 @@ func (mgr *ConfigManager) OpenConfig() (output string, v *viper.Viper, err error return output, nil, err } - contents, err = mgr.decryptConfig(mgr.masterpass, cfg) - if err != nil && err.Error() == "ciphertext is not a multiple of the block size" { - fmt.Println("Config wasn't encrypted.") - wasNotEncrypted = true + contents, err = decryptConfig(mgr.masterpass, cfg) + if err != nil && err.Error() == "cipher: message authentication failed" { + cipherAuthFailed = true } else if err != nil { return output, nil, err } - // v.AutomaticEnv() // read in environment variables that match - - // If a config file is found, read it in. - if err = mgr.v.ReadConfig(bytes.NewBuffer(contents)); err != nil { - return output, mgr.v, err + if err = mgr.v.ReadConfig(bytes.NewBuffer(contents)); err != nil && err.Error() == "While parsing config: (1, 1): unexpected token" { + return output, nil, fmt.Errorf("incorrect master password") + } else if err != nil { + return output, nil, err } - if wasNotEncrypted { + if cipherAuthFailed { + fmt.Println("Config wasn't encrypted, or has been compromised.") + if err = mgr.WriteConfig(); err != nil { return output, nil, err } @@ -86,7 +86,7 @@ func (mgr *ConfigManager) OpenConfig() (output string, v *viper.Viper, err error return mgr.masterpass, mgr.v, nil } -func (mgr ConfigManager) decryptConfig(masterpass string, cfgFile string) (contents []byte, err error) { +func decryptConfig(masterpass string, cfgFile string) (contents []byte, err error) { contents, err = ioutil.ReadFile(cfgFile) if err != nil { return contents, err @@ -97,10 +97,8 @@ func (mgr ConfigManager) decryptConfig(masterpass string, cfgFile string) (conte return contents, err } - plaintext, err := crypto.CBCDecrypt(passkey, contents) - if err != nil && err.Error() == "Padding incorrect" { - return contents, fmt.Errorf("incorrect master password") - } else if err != nil { + plaintext, err := crypto.GCMDecrypt(passkey, contents) + if err != nil { return contents, err } @@ -130,7 +128,7 @@ func (mgr ConfigManager) WriteConfig() (err error) { return err } - contents, err = crypto.CBCEncrypt(keypass, contents) + contents, err = crypto.GCMEncrypt(keypass, contents) if err != nil { return err } diff --git a/cmd/server/server.go b/cmd/server/server.go index 95f4ff5..4f07fbf 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -47,9 +47,14 @@ func main() { caPool.AppendCertsFromPEM([]byte(ca)) creds := credentials.NewTLS(&tls.Config{ - Certificates: []tls.Certificate{keypair}, - ClientCAs: caPool, - ClientAuth: tls.RequireAndVerifyClientCert, + Certificates: []tls.Certificate{keypair}, + ClientCAs: caPool, + ClientAuth: tls.RequireAndVerifyClientCert, + MinVersion: tls.VersionTLS12, + PreferServerCipherSuites: true, + CurvePreferences: []tls.CurveID{ + tls.CurveP256, + }, }) db, err := repositories.NewRedisConn("tcp", "redis:6379", 2) diff --git a/credentials/commands/cbc_to_gcm.go b/credentials/commands/cbc_to_gcm.go new file mode 100644 index 0000000..8796daa --- /dev/null +++ b/credentials/commands/cbc_to_gcm.go @@ -0,0 +1,94 @@ +package commands + +import ( + "context" + "encoding/base64" + "encoding/hex" + "fmt" + "time" + + "github.com/spf13/cobra" + + clitypes "github.com/mitchell/selfpass/cli/types" + "github.com/mitchell/selfpass/credentials/types" + "github.com/mitchell/selfpass/crypto" +) + +func MakeCBCtoGCM(repo clitypes.ConfigRepo, initClient CredentialClientInit) *cobra.Command { + cbcToGCM := &cobra.Command{ + Use: "cbc-to-gcm", + Hidden: true, + Run: func(cmd *cobra.Command, args []string) { + masterpass, cfg, err := repo.OpenConfig() + check(err) + + key, err := hex.DecodeString(cfg.GetString(clitypes.KeyPrivateKey)) + check(err) + + keypass, err := crypto.CombinePasswordAndKey([]byte(masterpass), key) + check(err) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) + defer cancel() + + client := initClient(ctx) + + mdch, errch := client.GetAllMetadata(ctx, "") + + for { + select { + case err := <-errch: + check(err) + case md, ok := <-mdch: + if !ok { + fmt.Println("All done.") + return + } + + cred, err := client.Get(ctx, md.ID) + check(err) + + passbytes, err := base64.StdEncoding.DecodeString(cred.Password) + check(err) + + plainpass, err := crypto.CBCDecrypt(keypass, passbytes) + check(err) + + passbytes, err = crypto.GCMEncrypt(keypass, plainpass) + check(err) + + cred.Password = base64.StdEncoding.EncodeToString(passbytes) + + if cred.OTPSecret != "" { + passbytes, err := base64.StdEncoding.DecodeString(cred.OTPSecret) + check(err) + + plainpass, err := crypto.CBCDecrypt(keypass, passbytes) + check(err) + + passbytes, err = crypto.GCMEncrypt(keypass, plainpass) + check(err) + + cred.OTPSecret = base64.StdEncoding.EncodeToString(passbytes) + } + + _, err = client.Update(ctx, cred.ID, types.CredentialInput{ + MetadataInput: types.MetadataInput{ + Tag: cred.Tag, + SourceHost: cred.SourceHost, + LoginURL: cred.LoginURL, + Primary: cred.Primary, + }, + OTPSecret: cred.OTPSecret, + Password: cred.Password, + Email: cred.Email, + Username: cred.Username, + }) + check(err) + } + } + }, + } + + return cbcToGCM +} diff --git a/credentials/commands/create.go b/credentials/commands/create.go index a3b874b..c18241d 100644 --- a/credentials/commands/create.go +++ b/credentials/commands/create.go @@ -99,7 +99,7 @@ password.`, } } - cipherpass, err := crypto.CBCEncrypt(keypass, []byte(ci.Password)) + cipherpass, err := crypto.GCMEncrypt(keypass, []byte(ci.Password)) check(err) ci.Password = base64.StdEncoding.EncodeToString(cipherpass) @@ -113,7 +113,7 @@ password.`, prompt := &survey.Password{Message: "OTP secret:"} check(survey.AskOne(prompt, &secret, nil)) - ciphersecret, err := crypto.CBCEncrypt(keypass, []byte(secret)) + ciphersecret, err := crypto.GCMEncrypt(keypass, []byte(secret)) check(err) ci.OTPSecret = base64.StdEncoding.EncodeToString(ciphersecret) diff --git a/credentials/commands/get.go b/credentials/commands/get.go index 46b05e9..2b14450 100644 --- a/credentials/commands/get.go +++ b/credentials/commands/get.go @@ -121,7 +121,7 @@ decrypting password.`, passbytes, err := base64.StdEncoding.DecodeString(cred.Password) check(err) - plainpass, err := crypto.CBCDecrypt(passkey, passbytes) + plainpass, err := crypto.GCMDecrypt(passkey, passbytes) check(clipboard.WriteAll(string(plainpass))) @@ -137,7 +137,7 @@ decrypting password.`, secretbytes, err := base64.StdEncoding.DecodeString(cred.OTPSecret) check(err) - plainsecret, err := crypto.CBCDecrypt(passkey, secretbytes) + plainsecret, err := crypto.GCMDecrypt(passkey, secretbytes) otp, err := totp.GenerateCode(string(plainsecret), time.Now()) check(err) diff --git a/credentials/repositories/grpc_client.go b/credentials/repositories/grpc_client.go index 63f7197..2dde63b 100644 --- a/credentials/repositories/grpc_client.go +++ b/credentials/repositories/grpc_client.go @@ -28,6 +28,10 @@ func NewCredentialServiceClient(ctx context.Context, target, ca, cert, key strin creds := credentials.NewTLS(&tls.Config{ RootCAs: capool, Certificates: []tls.Certificate{keypair}, + MinVersion: tls.VersionTLS12, + CurvePreferences: []tls.CurveID{ + tls.CurveP256, + }, }) conn, err := grpc.DialContext(ctx, target, grpc.WithTransportCredentials(creds), grpc.WithBlock()) diff --git a/crypto/gcm.go b/crypto/gcm.go new file mode 100644 index 0000000..5bc4b71 --- /dev/null +++ b/crypto/gcm.go @@ -0,0 +1,60 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "fmt" + "io" +) + +func GCMEncrypt(key []byte, plaintext []byte) ([]byte, error) { + if len(key) != 32 { + return nil, fmt.Errorf("key is not 32 bytes") + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, plaintext, nil), nil +} + +func GCMDecrypt(key []byte, ciphertext []byte) ([]byte, error) { + if len(key) != 32 { + return nil, fmt.Errorf("key is not 32 bytes") + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + if len(ciphertext) < gcm.NonceSize() { + return nil, errors.New("malformed ciphertext") + } + + return gcm.Open(nil, + ciphertext[:gcm.NonceSize()], + ciphertext[gcm.NonceSize():], + nil, + ) +}