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