commit bd900945f0f8c11f24a2e0046ae66a3d20744241
parent 907e15b47fb4cf86a806f4ee53f031f07e651a3c
Author: Sean Enck <sean@ttypty.com>
Date: Sat, 17 Sep 2022 12:21:42 -0400
more encrypt options
Diffstat:
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).