lockbox

password manager
Log | Files | Refs | README | LICENSE

commit bd900945f0f8c11f24a2e0046ae66a3d20744241
parent 907e15b47fb4cf86a806f4ee53f031f07e651a3c
Author: Sean Enck <sean@ttypty.com>
Date:   Sat, 17 Sep 2022 12:21:42 -0400

more encrypt options

Diffstat:
Ainternal/encrypt/aesgcm.go | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/encrypt/algorithms.go | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/encrypt/core.go | 93++++++++++++++++++++-----------------------------------------------------------
Minternal/encrypt/core_test.go | 36++++++++++++++++++++++++++++++++++++
Ainternal/encrypt/secretbox.go | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/inputs/env.go | 2++
6 files changed, 275 insertions(+), 70 deletions(-)

diff --git a/internal/encrypt/aesgcm.go b/internal/encrypt/aesgcm.go @@ -0,0 +1,75 @@ +package encrypt + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "io" +) + +const ( + aesGCMAlgorithmSaltLength = 32 +) + +type ( + aesGCMAlgorithm struct { + } +) + +func (a aesGCMAlgorithm) version() []byte { + return algoVersion(aesGCMAlgorithmVersion) +} + +func newCipher(key []byte, salt []byte) (cipher.Block, error) { + useKey, err := pad(salt, key) + if err != nil { + return nil, err + } + return aes.NewCipher(useKey[:]) +} + +func (a aesGCMAlgorithm) encrypt(key, data []byte) ([]byte, error) { + var salt [aesGCMAlgorithmSaltLength]byte + if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil { + return nil, err + } + c, err := newCipher(key, salt[:]) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + b := gcm.Seal(nonce, nonce, data, nil) //, nil + var d []byte + d = append(d, salt[:]...) + d = append(d, b...) + return d, nil +} + +func (a aesGCMAlgorithm) decrypt(key, encrypted []byte) ([]byte, error) { + var salt [aesGCMAlgorithmSaltLength]byte + copy(salt[:], encrypted[0:aesGCMAlgorithmSaltLength]) + c, err := newCipher(key, salt[:]) + if err != nil { + return nil, err + } + data := encrypted[aesGCMAlgorithmSaltLength:] + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + nonce := data[:nonceSize] + datum := data[nonceSize:] + + return gcm.Open(nil, nonce, datum, nil) +} diff --git a/internal/encrypt/algorithms.go b/internal/encrypt/algorithms.go @@ -0,0 +1,67 @@ +package encrypt + +import ( + "crypto/sha512" + "errors" + random "math/rand" + "time" + + "github.com/enckse/lockbox/internal/inputs" + "golang.org/x/crypto/pbkdf2" +) + +const ( + secretBoxAlgorithmVersion uint8 = 1 + isSecretBox = "secretbox" + aesGCMAlgorithmVersion uint8 = 2 +) + +type ( + algorithm interface { + encrypt(k, d []byte) ([]byte, error) + decrypt(k, d []byte) ([]byte, error) + version() []byte + } +) + +func init() { + random.Seed(time.Now().UnixNano()) +} + +func newAlgorithmFromVersion(vers uint8) algorithm { + switch vers { + case secretBoxAlgorithmVersion: + return secretBoxAlgorithm{} + case aesGCMAlgorithmVersion: + return aesGCMAlgorithm{} + } + return nil +} + +func newAlgorithm(mode string) algorithm { + useMode := mode + if mode == "" { + useMode = inputs.EnvOrDefault(inputs.EncryptModeEnv, isSecretBox) + } + switch useMode { + case isSecretBox: + return secretBoxAlgorithm{} + case "aes": + return aesGCMAlgorithm{} + } + return nil +} + +func algoVersion(v uint8) []byte { + return []byte{0, v} +} + +func pad(salt, key []byte) ([keyLength]byte, error) { + d := pbkdf2.Key(key, salt, 4096, keyLength, sha512.New) + if len(d) != keyLength { + return [keyLength]byte{}, errors.New("invalid key result from pad") + } + var obj [keyLength]byte + copy(obj[:], d[:keyLength]) + return obj, nil +} diff --git a/internal/encrypt/core.go b/internal/encrypt/core.go @@ -2,29 +2,14 @@ package encrypt import ( - "crypto/rand" - "crypto/sha512" "errors" - "io" - random "math/rand" "os" - "time" "github.com/enckse/lockbox/internal/inputs" - "golang.org/x/crypto/nacl/secretbox" - "golang.org/x/crypto/pbkdf2" ) const ( - keyLength = 32 - nonceLength = 24 - padLength = 256 - saltLength = 16 -) - -var ( - cryptoVers = []byte{0, 1} - cryptoVersLength = len(cryptoVers) + keyLength = 32 ) type ( @@ -32,13 +17,15 @@ type ( Lockbox struct { secret [keyLength]byte file string + algo string } // LockboxOptions represent options to create a lockbox from. LockboxOptions struct { - Key string - KeyMode string - File string + Key string + KeyMode string + File string + Algorithm string } ) @@ -62,40 +49,21 @@ func ToFile(file string, data []byte) error { // NewLockbox creates a new usable lockbox instance. func NewLockbox(options LockboxOptions) (Lockbox, error) { - return newLockbox(options.Key, options.KeyMode, options.File) + return newLockbox(options.Key, options.KeyMode, options.File, options.Algorithm) } -func newLockbox(key, keyMode, file string) (Lockbox, error) { +func newLockbox(key, keyMode, file, algo string) (Lockbox, error) { b, err := inputs.GetKey(key, keyMode) if err != nil { return Lockbox{}, err } var secretKey [keyLength]byte copy(secretKey[:], b) - return Lockbox{secret: secretKey, file: file}, nil -} - -func pad(salt, key []byte) ([keyLength]byte, error) { - d := pbkdf2.Key(key, salt, 4096, keyLength, sha512.New) - if len(d) != keyLength { - return [keyLength]byte{}, errors.New("invalid key result from pad") - } - var obj [keyLength]byte - copy(obj[:], d[:keyLength]) - return obj, nil -} - -func init() { - random.Seed(time.Now().UnixNano()) + return Lockbox{secret: secretKey, file: file, algo: algo}, nil } // Encrypt will encrypt contents to file. func (l Lockbox) Encrypt(datum []byte) error { - var nonce [nonceLength]byte - padTo := random.Intn(padLength) - if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { - return err - } data := datum if data == nil { b, err := inputs.RawStdin() @@ -104,49 +72,34 @@ func (l Lockbox) Encrypt(datum []byte) error { } data = b } - var padding [padLength]byte - if _, err := io.ReadFull(rand.Reader, padding[:]); err != nil { - return err - } - var salt [saltLength]byte - if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil { - return err + box := newAlgorithm(l.algo) + if box == nil { + return errors.New("unknown algorithm detected") } - var write []byte - write = append(write, byte(padTo)) - write = append(write, padding[0:padTo]...) - write = append(write, data...) - key, err := pad(salt[:], l.secret[:]) + b, err := box.encrypt(l.secret[:], data) if err != nil { return err } - encrypted := secretbox.Seal(nonce[:], write, &nonce, &key) var persist []byte - persist = append(persist, cryptoVers...) - persist = append(persist, salt[:]...) - persist = append(persist, encrypted...) + persist = append(persist, box.version()...) + persist = append(persist, b...) return os.WriteFile(l.file, persist, 0600) } // Decrypt will decrypt an object from file. func (l Lockbox) Decrypt() ([]byte, error) { - var nonce [nonceLength]byte - var salt [saltLength]byte encrypted, err := os.ReadFile(l.file) if err != nil { return nil, err } - copy(salt[:], encrypted[cryptoVersLength:saltLength+cryptoVersLength]) - copy(nonce[:], encrypted[cryptoVersLength+saltLength:cryptoVersLength+saltLength+nonceLength]) - key, err := pad(salt[:], l.secret[:]) - if err != nil { - return nil, err + version := len(algoVersion(0)) + if len(encrypted) <= version { + return nil, errors.New("invalid decryption data") } - decrypted, ok := secretbox.Open(nil, encrypted[cryptoVersLength+saltLength+nonceLength:], &nonce, &key) - if !ok { - return nil, errors.New("decrypt not ok") + data := encrypted[version:] + box := newAlgorithmFromVersion(encrypted[1]) + if box == nil { + return nil, errors.New("unable to detect algorithm") } - - padding := int(decrypted[0]) - return decrypted[1+padding:], nil + return box.decrypt(l.secret[:], data) } diff --git a/internal/encrypt/core_test.go b/internal/encrypt/core_test.go @@ -91,3 +91,39 @@ func TestEncryptDecryptPlainText(t *testing.T) { t.Error("data mismatch") } } + +func TestEncryptDecryptSecretBox(t *testing.T) { + e, err := encrypt.NewLockbox(encrypt.LockboxOptions{Key: "plain", KeyMode: inputs.PlainKeyMode, File: setupData(t), Algorithm: "secretbox"}) + if err != nil { + t.Errorf("failed to create lockbox: %v", err) + } + data := []byte("datum") + if err := e.Encrypt(data); err != nil { + t.Errorf("failed to encrypt: %v", err) + } + d, err := e.Decrypt() + if err != nil { + t.Errorf("failed to decrypt: %v", err) + } + if string(d) != string(data) { + t.Error("data mismatch") + } +} + +func TestEncryptDecryptAESBox(t *testing.T) { + e, err := encrypt.NewLockbox(encrypt.LockboxOptions{Key: "plain", KeyMode: inputs.PlainKeyMode, File: setupData(t), Algorithm: "aes"}) + if err != nil { + t.Errorf("failed to create lockbox: %v", err) + } + data := []byte("datum") + if err := e.Encrypt(data); err != nil { + t.Errorf("failed to encrypt: %v", err) + } + d, err := e.Decrypt() + if err != nil { + t.Errorf("failed to decrypt: %v", err) + } + if string(d) != string(data) { + t.Error("data mismatch") + } +} diff --git a/internal/encrypt/secretbox.go b/internal/encrypt/secretbox.go @@ -0,0 +1,72 @@ +package encrypt + +import ( + "crypto/rand" + "errors" + "io" + random "math/rand" + + "golang.org/x/crypto/nacl/secretbox" +) + +type ( + secretBoxAlgorithm struct { + } +) + +const ( + secretBoxAlgorithmNonceLength = 24 + secretBoxAlgorithmPadLength = 256 + secretBoxAlgorithmSaltLength = 16 +) + +func (s secretBoxAlgorithm) version() []byte { + return algoVersion(secretBoxAlgorithmVersion) +} + +func (s secretBoxAlgorithm) encrypt(encryptKey, data []byte) ([]byte, error) { + var nonce [secretBoxAlgorithmNonceLength]byte + padTo := random.Intn(secretBoxAlgorithmPadLength) + if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { + return nil, err + } + var padding [secretBoxAlgorithmPadLength]byte + if _, err := io.ReadFull(rand.Reader, padding[:]); err != nil { + return nil, err + } + var salt [secretBoxAlgorithmSaltLength]byte + if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil { + return nil, err + } + var write []byte + write = append(write, byte(padTo)) + write = append(write, padding[0:padTo]...) + write = append(write, data...) + key, err := pad(salt[:], encryptKey[:]) + if err != nil { + return nil, err + } + encrypted := secretbox.Seal(nonce[:], write, &nonce, &key) + var persist []byte + persist = append(persist, salt[:]...) + persist = append(persist, encrypted...) + return persist, nil +} + +func (s secretBoxAlgorithm) decrypt(encryptKey, encrypted []byte) ([]byte, error) { + var nonce [secretBoxAlgorithmNonceLength]byte + var salt [secretBoxAlgorithmSaltLength]byte + copy(salt[:], encrypted[0:secretBoxAlgorithmSaltLength]) + copy(nonce[:], encrypted[secretBoxAlgorithmSaltLength:secretBoxAlgorithmSaltLength+secretBoxAlgorithmNonceLength]) + key, err := pad(salt[:], encryptKey[:]) + if err != nil { + return nil, err + } + decrypted, ok := secretbox.Open(nil, encrypted[secretBoxAlgorithmSaltLength+secretBoxAlgorithmNonceLength:], &nonce, &key) + if !ok { + return nil, errors.New("decrypt not ok") + } + + padding := int(decrypted[0]) + return decrypted[1+padding:], nil +} diff --git a/internal/inputs/env.go b/internal/inputs/env.go @@ -34,6 +34,8 @@ const ( ClipMaxEnv = prefixKey + "CLIPMAX" // ColorBetweenEnv is a comma-delimited list of times to color totp outputs (e.g. 0:5,30:35 which is the default). ColorBetweenEnv = prefixKey + "TOTPBETWEEN" + // EncryptModeEnv indicates the underlying algorith to use for encryption. + EncryptModeEnv = prefixKey + "ALGORITHM" // PlainKeyMode is plaintext based key resolution. PlainKeyMode = "plaintext" // CommandKeyMode will run an external command to get the key (from stdout).