Swapped AES-CBC for GCM for all symmetric encryption; bolstered TLS configs

This commit is contained in:
mitchell 2019-06-07 02:03:15 -07:00
parent cde1d118fc
commit f90c19d0f4
11 changed files with 192 additions and 30 deletions

View File

@ -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. 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 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 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-CBC a *private key* and *master password*. All of which is done using mutual TLS and an AES-GCM
encrypted config. encrypted config.
#### Service Roadmap #### Service Roadmap
@ -31,8 +31,8 @@ encrypted config.
| Support credentials CRUD via gRPC. | 80% | TODO: Update | | Support credentials CRUD via gRPC. | 80% | TODO: Update |
| Support mutual TLS. | 100% | | | Support mutual TLS. | 100% | |
| Support storage of certs, PK, and host in AES-CBC encrypted config. | 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-GCM 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 local files, using MP and PK. | 100% | |
#### Unplanned Goals #### Unplanned Goals

View File

@ -40,7 +40,7 @@ the new file.`,
passkey, err := crypto.CombinePasswordAndKey([]byte(masterpass), []byte(key)) passkey, err := crypto.CombinePasswordAndKey([]byte(masterpass), []byte(key))
check(err) check(err)
contents, err = crypto.CBCDecrypt(passkey, contents) contents, err = crypto.GCMDecrypt(passkey, contents)
check(err) check(err)
check(ioutil.WriteFile(fileout, contents, 0600)) check(ioutil.WriteFile(fileout, contents, 0600))

View File

@ -36,7 +36,7 @@ new file.`,
passkey, err := crypto.CombinePasswordAndKey([]byte(masterpass), []byte(key)) passkey, err := crypto.CombinePasswordAndKey([]byte(masterpass), []byte(key))
check(err) check(err)
contents, err = crypto.CBCEncrypt(passkey, contents) contents, err = crypto.GCMEncrypt(passkey, contents)
check(err) check(err)
check(ioutil.WriteFile(fileEnc, contents, 0600)) check(ioutil.WriteFile(fileEnc, contents, 0600))

View File

@ -36,6 +36,7 @@ can interact with the entire Selfpass API.`,
rootCmd.AddCommand(commands.MakeCreate(mgr, makeInitClient(mgr, clientInit))) rootCmd.AddCommand(commands.MakeCreate(mgr, makeInitClient(mgr, clientInit)))
rootCmd.AddCommand(commands.MakeGet(mgr, makeInitClient(mgr, clientInit))) rootCmd.AddCommand(commands.MakeGet(mgr, makeInitClient(mgr, clientInit)))
rootCmd.AddCommand(commands.MakeDelete(makeInitClient(mgr, clientInit))) rootCmd.AddCommand(commands.MakeDelete(makeInitClient(mgr, clientInit)))
rootCmd.AddCommand(commands.MakeCBCtoGCM(mgr, makeInitClient(mgr, clientInit)))
check(rootCmd.Execute()) check(rootCmd.Execute())
} }

View File

@ -51,7 +51,7 @@ func (mgr *ConfigManager) OpenConfig() (output string, v *viper.Viper, err error
mgr.cfgFile = &cfg mgr.cfgFile = &cfg
var contents []byte var contents []byte
var wasNotEncrypted bool var cipherAuthFailed bool
if _, err := os.Open(cfg); os.IsNotExist(err) { if _, err := os.Open(cfg); os.IsNotExist(err) {
return output, mgr.v, fmt.Errorf("no config found, run 'init' command") 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 return output, nil, err
} }
contents, err = mgr.decryptConfig(mgr.masterpass, cfg) contents, err = decryptConfig(mgr.masterpass, cfg)
if err != nil && err.Error() == "ciphertext is not a multiple of the block size" { if err != nil && err.Error() == "cipher: message authentication failed" {
fmt.Println("Config wasn't encrypted.") cipherAuthFailed = true
wasNotEncrypted = true
} else if err != nil { } else if err != nil {
return output, nil, err return output, nil, err
} }
// v.AutomaticEnv() // read in environment variables that match 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")
// If a config file is found, read it in. } else if err != nil {
if err = mgr.v.ReadConfig(bytes.NewBuffer(contents)); err != nil { return output, nil, err
return output, mgr.v, err
} }
if wasNotEncrypted { if cipherAuthFailed {
fmt.Println("Config wasn't encrypted, or has been compromised.")
if err = mgr.WriteConfig(); err != nil { if err = mgr.WriteConfig(); err != nil {
return output, nil, err 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 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) contents, err = ioutil.ReadFile(cfgFile)
if err != nil { if err != nil {
return contents, err return contents, err
@ -97,10 +97,8 @@ func (mgr ConfigManager) decryptConfig(masterpass string, cfgFile string) (conte
return contents, err return contents, err
} }
plaintext, err := crypto.CBCDecrypt(passkey, contents) plaintext, err := crypto.GCMDecrypt(passkey, contents)
if err != nil && err.Error() == "Padding incorrect" { if err != nil {
return contents, fmt.Errorf("incorrect master password")
} else if err != nil {
return contents, err return contents, err
} }
@ -130,7 +128,7 @@ func (mgr ConfigManager) WriteConfig() (err error) {
return err return err
} }
contents, err = crypto.CBCEncrypt(keypass, contents) contents, err = crypto.GCMEncrypt(keypass, contents)
if err != nil { if err != nil {
return err return err
} }

View File

@ -50,6 +50,11 @@ func main() {
Certificates: []tls.Certificate{keypair}, Certificates: []tls.Certificate{keypair},
ClientCAs: caPool, ClientCAs: caPool,
ClientAuth: tls.RequireAndVerifyClientCert, ClientAuth: tls.RequireAndVerifyClientCert,
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
CurvePreferences: []tls.CurveID{
tls.CurveP256,
},
}) })
db, err := repositories.NewRedisConn("tcp", "redis:6379", 2) db, err := repositories.NewRedisConn("tcp", "redis:6379", 2)

View File

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

View File

@ -99,7 +99,7 @@ password.`,
} }
} }
cipherpass, err := crypto.CBCEncrypt(keypass, []byte(ci.Password)) cipherpass, err := crypto.GCMEncrypt(keypass, []byte(ci.Password))
check(err) check(err)
ci.Password = base64.StdEncoding.EncodeToString(cipherpass) ci.Password = base64.StdEncoding.EncodeToString(cipherpass)
@ -113,7 +113,7 @@ password.`,
prompt := &survey.Password{Message: "OTP secret:"} prompt := &survey.Password{Message: "OTP secret:"}
check(survey.AskOne(prompt, &secret, nil)) check(survey.AskOne(prompt, &secret, nil))
ciphersecret, err := crypto.CBCEncrypt(keypass, []byte(secret)) ciphersecret, err := crypto.GCMEncrypt(keypass, []byte(secret))
check(err) check(err)
ci.OTPSecret = base64.StdEncoding.EncodeToString(ciphersecret) ci.OTPSecret = base64.StdEncoding.EncodeToString(ciphersecret)

View File

@ -121,7 +121,7 @@ decrypting password.`,
passbytes, err := base64.StdEncoding.DecodeString(cred.Password) passbytes, err := base64.StdEncoding.DecodeString(cred.Password)
check(err) check(err)
plainpass, err := crypto.CBCDecrypt(passkey, passbytes) plainpass, err := crypto.GCMDecrypt(passkey, passbytes)
check(clipboard.WriteAll(string(plainpass))) check(clipboard.WriteAll(string(plainpass)))
@ -137,7 +137,7 @@ decrypting password.`,
secretbytes, err := base64.StdEncoding.DecodeString(cred.OTPSecret) secretbytes, err := base64.StdEncoding.DecodeString(cred.OTPSecret)
check(err) check(err)
plainsecret, err := crypto.CBCDecrypt(passkey, secretbytes) plainsecret, err := crypto.GCMDecrypt(passkey, secretbytes)
otp, err := totp.GenerateCode(string(plainsecret), time.Now()) otp, err := totp.GenerateCode(string(plainsecret), time.Now())
check(err) check(err)

View File

@ -28,6 +28,10 @@ func NewCredentialServiceClient(ctx context.Context, target, ca, cert, key strin
creds := credentials.NewTLS(&tls.Config{ creds := credentials.NewTLS(&tls.Config{
RootCAs: capool, RootCAs: capool,
Certificates: []tls.Certificate{keypair}, Certificates: []tls.Certificate{keypair},
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{
tls.CurveP256,
},
}) })
conn, err := grpc.DialContext(ctx, target, grpc.WithTransportCredentials(creds), grpc.WithBlock()) conn, err := grpc.DialContext(ctx, target, grpc.WithTransportCredentials(creds), grpc.WithBlock())

60
crypto/gcm.go Normal file
View File

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