mirror of https://github.com/mitchell/selfpass.git
Swapped AES-CBC for GCM for all symmetric encryption; bolstered TLS configs
This commit is contained in:
parent
cde1d118fc
commit
f90c19d0f4
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue