From 3ce0fa2a759d5ff7353d3ca3b50cfac786482bef Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Sun, 23 Jul 2023 20:44:42 -0500 Subject: [PATCH 01/17] Adding key generation adapted from passlock --- go.mod | 7 ++- go.sum | 2 + pkg/passlock/doc.go | 18 ++++++ pkg/passlock/key.go | 124 +++++++++++++++++++++++++++++++++++++++ pkg/passlock/key_test.go | 44 ++++++++++++++ 5 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 pkg/passlock/doc.go create mode 100644 pkg/passlock/key.go create mode 100644 pkg/passlock/key_test.go diff --git a/go.mod b/go.mod index ffd7ee0..e9f7aa1 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,14 @@ module github.com/saylorsolutions/gocryptx go 1.19 -require github.com/stretchr/testify v1.8.4 +require ( + github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.8.4 + golang.org/x/crypto v0.11.0 +) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 01fa189..a49392a 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/passlock/doc.go b/pkg/passlock/doc.go new file mode 100644 index 0000000..f9daf24 --- /dev/null +++ b/pkg/passlock/doc.go @@ -0,0 +1,18 @@ +/* +Package passlock provides functions for encrypting data using a key derived from a user-provided passphrase. +This uses AES-256 encryption to encrypt the provided data. + +# How it works: + +A key and salt is generated from the given password. The salt is prepended to the encrypted payload so the same key can be derived later, given the same passphrase. +Scrypt is memory and CPU hard, so it's impractical to brute force the hash to get the original value, provided that sufficient tuning values are provided to the KeyGenerator. + +The key and salt are passed to the Encrypt function to use the key to encrypt the payload and prepend the salt. +The passphrase is passed to the Decrypt function to derive the key from the passphrase and salt, and decrypt the payload. + +# General guidelines: + - AES-512 is better for high security applications, while AES-256 is a good default for most cases. + - It's possible to customize the CPU cost, iteration count, and relative block size parameters directly. If you're not an expert, then don't use SetIterations, SetCPUCost, or SetRelativeBlockSize. + - Both short and long delay iteration GeneratorOpt functions are provided, choose the correct iterations for your use-case using either SetLongDelayIterations or SetShortDelayIterations. +*/ +package passlock diff --git a/pkg/passlock/key.go b/pkg/passlock/key.go new file mode 100644 index 0000000..a6536e5 --- /dev/null +++ b/pkg/passlock/key.go @@ -0,0 +1,124 @@ +package passlock + +import ( + "crypto/rand" + "errors" + "golang.org/x/crypto/scrypt" +) + +const ( + DefaultLargeIterations = 1_048_576 + DefaultInteractiveIterations = 131_072 + DefaultRelBlockSize = 8 + DefaultCpuCost = 1 + AES256KeySize = 256 / 8 + AES512KeySize = 512 / 8 +) + +type KeyGenerator struct { + iterations int + relativeBlockSize int + cpuCost int + aesKeySize int +} + +type GeneratorOpt = func(*KeyGenerator) error + +func SetAES256KeySize() GeneratorOpt { + return func(gen *KeyGenerator) error { + gen.aesKeySize = AES256KeySize + return nil + } +} + +func SetAES512KeySize() GeneratorOpt { + return func(gen *KeyGenerator) error { + gen.aesKeySize = AES512KeySize + return nil + } +} + +// SetLongDelayIterations sets a higher iteration count. This is sufficient for infrequent key derivation, or cases where the key will be cached for long periods of time. +// This option is much more resistant to password cracking, and is the default. +func SetLongDelayIterations() GeneratorOpt { + return func(gen *KeyGenerator) error { + gen.iterations = DefaultLargeIterations + return nil + } +} + +// SetShortDelayIterations sets a lower iteration count. This is appropriate for situations where a shorter delay is desired because of frequent key derivations. +// This option balances speed with password cracking resistance. It's recommended to use longer passwords with this approach. +func SetShortDelayIterations() GeneratorOpt { + return func(gen *KeyGenerator) error { + gen.iterations = DefaultInteractiveIterations + return nil + } +} + +// SetIterations allows the caller to customize the iteration count. +// Only use this option if you know what you're doing. +func SetIterations(iterations int) GeneratorOpt { + return func(gen *KeyGenerator) error { + if iterations <= 1 { + return errors.New("iterations cannot be <= 1") + } + if iterations%2 != 0 { + return errors.New("iterations must be a power of 2") + } + gen.iterations = iterations + return nil + } +} + +// SetCPUCost sets the parallelism factor for key generation from the default of 1. +// Only use this option if you know what you're doing. +func SetCPUCost(cost int) GeneratorOpt { + return func(gen *KeyGenerator) error { + if cost < DefaultCpuCost { + return errors.New("cpu cost must be at least 1") + } + gen.cpuCost = cost + return nil + } +} + +// SetRelativeBlockSize sets the relative block size. +// Only use this option if you know what you're doing. +func SetRelativeBlockSize(size int) GeneratorOpt { + return func(gen *KeyGenerator) error { + if size < DefaultRelBlockSize { + return errors.New("relative block size must be at least 8") + } + gen.relativeBlockSize = size + return nil + } +} + +// NewKeyGenerator creates a new KeyGenerator using the options provided as zero or more GeneratorOpt. +// By default, the generator generates a key for AES256KeySize using DefaultLargeIterations. +func NewKeyGenerator(opts ...GeneratorOpt) (*KeyGenerator, error) { + gen := &KeyGenerator{ + iterations: DefaultLargeIterations, + relativeBlockSize: DefaultRelBlockSize, + cpuCost: DefaultCpuCost, + aesKeySize: AES256KeySize, + } + + for _, opt := range opts { + if err := opt(gen); err != nil { + return nil, err + } + } + return gen, nil +} + +// GenerateKey will generate an AES key and salt using the configuration of the KeyGenerator. +func (g *KeyGenerator) GenerateKey(pass []byte) (key, salt []byte, err error) { + salt = make([]byte, g.aesKeySize) + if _, err = rand.Read(salt); err != nil { + return nil, nil, err + } + key, err = scrypt.Key(pass, salt, g.iterations, g.relativeBlockSize, g.cpuCost, g.aesKeySize) + return key, salt, err +} diff --git a/pkg/passlock/key_test.go b/pkg/passlock/key_test.go new file mode 100644 index 0000000..425931d --- /dev/null +++ b/pkg/passlock/key_test.go @@ -0,0 +1,44 @@ +package passlock + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewKeyGenerator(t *testing.T) { + gen, err := NewKeyGenerator(SetShortDelayIterations()) + assert.NoError(t, err) + assert.NotNil(t, gen) + assert.Equal(t, DefaultInteractiveIterations, gen.iterations) + assert.Equal(t, DefaultCpuCost, gen.cpuCost) + assert.Equal(t, AES256KeySize, gen.aesKeySize) + assert.Equal(t, DefaultRelBlockSize, gen.relativeBlockSize) + + key, salt, err := gen.GenerateKey([]byte("a test password")) + assert.NoError(t, err) + assert.Len(t, key, gen.aesKeySize) + assert.Len(t, salt, gen.aesKeySize) +} + +func TestNewKeyGenerator_Custom(t *testing.T) { + gen, err := NewKeyGenerator( + SetIterations(2), + SetLongDelayIterations(), + SetShortDelayIterations(), + SetCPUCost(DefaultCpuCost), + SetRelativeBlockSize(DefaultRelBlockSize), + SetAES512KeySize(), + SetAES256KeySize(), + ) + assert.NoError(t, err) + assert.NotNil(t, gen) + assert.Equal(t, DefaultInteractiveIterations, gen.iterations) + assert.Equal(t, DefaultCpuCost, gen.cpuCost) + assert.Equal(t, AES256KeySize, gen.aesKeySize) + assert.Equal(t, DefaultRelBlockSize, gen.relativeBlockSize) + + key, salt, err := gen.GenerateKey([]byte("a test password")) + assert.NoError(t, err) + assert.Len(t, key, gen.aesKeySize) + assert.Len(t, salt, gen.aesKeySize) +} From f78e4f43cbf9f71b64901a952747912717354572 Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Mon, 24 Jul 2023 00:27:45 -0500 Subject: [PATCH 02/17] Adding a Derive method to KeyGenerator - Adding some error handling for passphrase being blank, and source data not being long enough to contain both a salt and ciphertext. --- pkg/passlock/key.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pkg/passlock/key.go b/pkg/passlock/key.go index a6536e5..3f8c5c1 100644 --- a/pkg/passlock/key.go +++ b/pkg/passlock/key.go @@ -3,6 +3,7 @@ package passlock import ( "crypto/rand" "errors" + "fmt" "golang.org/x/crypto/scrypt" ) @@ -15,6 +16,11 @@ const ( AES512KeySize = 512 / 8 ) +var ( + ErrEmptyPassPhrase = errors.New("cannot use an empty passphrase") + ErrInvalidData = errors.New("unable to use input data") +) + type KeyGenerator struct { iterations int relativeBlockSize int @@ -115,6 +121,9 @@ func NewKeyGenerator(opts ...GeneratorOpt) (*KeyGenerator, error) { // GenerateKey will generate an AES key and salt using the configuration of the KeyGenerator. func (g *KeyGenerator) GenerateKey(pass []byte) (key, salt []byte, err error) { + if len(pass) == 0 { + return nil, nil, ErrEmptyPassPhrase + } salt = make([]byte, g.aesKeySize) if _, err = rand.Read(salt); err != nil { return nil, nil, err @@ -122,3 +131,14 @@ func (g *KeyGenerator) GenerateKey(pass []byte) (key, salt []byte, err error) { key, err = scrypt.Key(pass, salt, g.iterations, g.relativeBlockSize, g.cpuCost, g.aesKeySize) return key, salt, err } + +func (g *KeyGenerator) DeriveKey(pass, data []byte) (key []byte, err error) { + if len(pass) == 0 { + return nil, ErrEmptyPassPhrase + } + if len(data) <= g.aesKeySize { + return nil, fmt.Errorf("%w: input data isn't long enough to contain a key salt", ErrInvalidData) + } + salt := data[len(data)-g.aesKeySize:] + return scrypt.Key(pass, salt, g.iterations, g.relativeBlockSize, g.cpuCost, g.aesKeySize) +} From bf90771296c1c212e7efdfd1becdab93b05d1d8d Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Mon, 24 Jul 2023 02:00:24 -0500 Subject: [PATCH 03/17] AES-512 isn't supported by the stdlib, but 256 is still pretty good - Adding to the documentation to explain what's happening here. --- pkg/passlock/doc.go | 15 +++++++++------ pkg/passlock/key.go | 18 ++++++++---------- pkg/passlock/key_test.go | 1 - 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pkg/passlock/doc.go b/pkg/passlock/doc.go index f9daf24..7f3ee2f 100644 --- a/pkg/passlock/doc.go +++ b/pkg/passlock/doc.go @@ -4,15 +4,18 @@ This uses AES-256 encryption to encrypt the provided data. # How it works: -A key and salt is generated from the given password. The salt is prepended to the encrypted payload so the same key can be derived later, given the same passphrase. -Scrypt is memory and CPU hard, so it's impractical to brute force the hash to get the original value, provided that sufficient tuning values are provided to the KeyGenerator. +A key and salt is generated from the given passphrase. The salt is appended to the encrypted payload so the same key can be derived later given the same passphrase. +Scrypt is memory and CPU hard, so it's impractical to brute force the salt to get the original passphrase, provided that sufficient tuning values are provided to the KeyGenerator. -The key and salt are passed to the Encrypt function to use the key to encrypt the payload and prepend the salt. -The passphrase is passed to the Decrypt function to derive the key from the passphrase and salt, and decrypt the payload. +The key, salt, and plaintext are passed to the Lock function to encrypt the payload and append the salt to it. +The key is recovered from the encrypted payload by passing the original passphrase and the payload to KeyGenerator.Derive. +The key and encrypted payload are passed to the Unlock function to decrypt the payload and return the original plain text. # General guidelines: - - AES-512 is better for high security applications, while AES-256 is a good default for most cases. - - It's possible to customize the CPU cost, iteration count, and relative block size parameters directly. If you're not an expert, then don't use SetIterations, SetCPUCost, or SetRelativeBlockSize. + - It's possible to customize the CPU cost, iteration count, and relative block size parameters directly for key generation. If you're not an expert, then don't use SetIterations, SetCPUCost, or SetRelativeBlockSize. - Both short and long delay iteration GeneratorOpt functions are provided, choose the correct iterations for your use-case using either SetLongDelayIterations or SetShortDelayIterations. + - This method of encryption (AES256GCM) supports encrypting and authenticating at most about 64GB at a time. You could get around this by splitting a very large file into multiple chunks that include some metadata to prevent reordering or truncating. + - AES-256 is a good default for a lot of cases, with excellent security and good throughput speeds. This library only supports AES-256 since that is the best supported by the Go standard lib, and it conforms to the constraints posed by AES in general. + - When deriving the key from an encrypted payload, make sure that the same KeyGenerator settings are used. Not doing so will likely result in an incorrect key. */ package passlock diff --git a/pkg/passlock/key.go b/pkg/passlock/key.go index 3f8c5c1..c7815f7 100644 --- a/pkg/passlock/key.go +++ b/pkg/passlock/key.go @@ -13,7 +13,6 @@ const ( DefaultRelBlockSize = 8 DefaultCpuCost = 1 AES256KeySize = 256 / 8 - AES512KeySize = 512 / 8 ) var ( @@ -37,13 +36,6 @@ func SetAES256KeySize() GeneratorOpt { } } -func SetAES512KeySize() GeneratorOpt { - return func(gen *KeyGenerator) error { - gen.aesKeySize = AES512KeySize - return nil - } -} - // SetLongDelayIterations sets a higher iteration count. This is sufficient for infrequent key derivation, or cases where the key will be cached for long periods of time. // This option is much more resistant to password cracking, and is the default. func SetLongDelayIterations() GeneratorOpt { @@ -132,6 +124,7 @@ func (g *KeyGenerator) GenerateKey(pass []byte) (key, salt []byte, err error) { return key, salt, err } +// DeriveKey will recover a key with the salt in the payload and the given passphrase. func (g *KeyGenerator) DeriveKey(pass, data []byte) (key []byte, err error) { if len(pass) == 0 { return nil, ErrEmptyPassPhrase @@ -139,6 +132,11 @@ func (g *KeyGenerator) DeriveKey(pass, data []byte) (key []byte, err error) { if len(data) <= g.aesKeySize { return nil, fmt.Errorf("%w: input data isn't long enough to contain a key salt", ErrInvalidData) } - salt := data[len(data)-g.aesKeySize:] - return scrypt.Key(pass, salt, g.iterations, g.relativeBlockSize, g.cpuCost, g.aesKeySize) + var salt []byte + salt = data[len(data)-g.aesKeySize:] + key, err = scrypt.Key(pass, salt, g.iterations, g.relativeBlockSize, g.cpuCost, g.aesKeySize) + if err != nil { + return nil, err + } + return key, nil } diff --git a/pkg/passlock/key_test.go b/pkg/passlock/key_test.go index 425931d..52bde67 100644 --- a/pkg/passlock/key_test.go +++ b/pkg/passlock/key_test.go @@ -27,7 +27,6 @@ func TestNewKeyGenerator_Custom(t *testing.T) { SetShortDelayIterations(), SetCPUCost(DefaultCpuCost), SetRelativeBlockSize(DefaultRelBlockSize), - SetAES512KeySize(), SetAES256KeySize(), ) assert.NoError(t, err) From f09f7ae2868ab009c2f0e7ae06a29e751ca40459 Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Mon, 24 Jul 2023 02:13:46 -0500 Subject: [PATCH 04/17] Adding Lock and Unlock functions --- pkg/passlock/locker.go | 47 +++++++++++++++++++++++++++++++++++++ pkg/passlock/locker_test.go | 32 +++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 pkg/passlock/locker.go create mode 100644 pkg/passlock/locker_test.go diff --git a/pkg/passlock/locker.go b/pkg/passlock/locker.go new file mode 100644 index 0000000..1b8ca44 --- /dev/null +++ b/pkg/passlock/locker.go @@ -0,0 +1,47 @@ +package passlock + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" +) + +// Lock will encrypt the payload with the given key, and append the given salt to the payload. +// Exposure of the salt doesn't weaken the key, since the passphrase is also required to arrive at the same key. +// However, tampering with the salt or the payload would prevent Unlock from recovering the plaintext payload. +func Lock(key, salt, data []byte) ([]byte, error) { + 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()) + if _, err := rand.Read(nonce); err != nil { + return nil, err + } + + cipherText := append(gcm.Seal(nonce, nonce, data, nil), salt...) + return cipherText, nil +} + +// Unlock will decrypt the payload after stripping the salt from the end of it. +// The salt length is expected to match the key length (which is enforced by KeyGenerator). +func Unlock(key, data []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + nonce, cipherText := data[:nonceSize], data[nonceSize:] + cipherText = cipherText[:len(cipherText)-len(key)] + return gcm.Open(nil, nonce, cipherText, nil) +} diff --git a/pkg/passlock/locker_test.go b/pkg/passlock/locker_test.go new file mode 100644 index 0000000..4ccc28c --- /dev/null +++ b/pkg/passlock/locker_test.go @@ -0,0 +1,32 @@ +package passlock + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestLockUnlock(t *testing.T) { + const password = "password" + const data = "How wonderful life is while you're in the world" + dataBytes := []byte(data) + + gen, err := NewKeyGenerator(SetShortDelayIterations()) + assert.NoError(t, err) + key, salt, err := gen.GenerateKey([]byte(password)) + assert.NoError(t, err) + + encrypted, err := Lock(key, salt, dataBytes) + t.Log(string(encrypted)) + assert.NoError(t, err) + assert.NotEqual(t, dataBytes, encrypted) + + key2, err := gen.DeriveKey([]byte(password), encrypted) + assert.NoError(t, err) + assert.Equal(t, key, key2) + + unencrypted, err := Unlock(key2, encrypted) + t.Log(string(unencrypted)) + assert.NoError(t, err) + assert.NotEqual(t, encrypted, unencrypted) + assert.Equal(t, data, string(unencrypted)) +} From 7d7c40df1d1964c803bbc8dc8a98a80e11a861dd Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Mon, 24 Jul 2023 17:26:46 -0500 Subject: [PATCH 05/17] WIP: Moving to Multilocker instead, need to take a beat to create an easier interface for binary scanning --- pkg/passlock/multilock.go | 135 +++++++++++++++++++++++++++++++++ pkg/passlock/multilock_test.go | 24 ++++++ pkg/passlock/multilocker.go | 19 +++++ 3 files changed, 178 insertions(+) create mode 100644 pkg/passlock/multilock.go create mode 100644 pkg/passlock/multilock_test.go create mode 100644 pkg/passlock/multilocker.go diff --git a/pkg/passlock/multilock.go b/pkg/passlock/multilock.go new file mode 100644 index 0000000..6686676 --- /dev/null +++ b/pkg/passlock/multilock.go @@ -0,0 +1,135 @@ +package passlock + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" +) + +var ( + ErrInvalidHeader = errors.New("invalid MultiLock header") +) + +// MultiLock is used to allow multiple distinct passphrases to decrypt an encrypted payload. +type MultiLock struct { + numKeys int + id [][idFieldLen]byte + len []int + encryptedKey [][]byte + gen *KeyGenerator +} + +func NewMultiLocker() (*MultiLock, error) { + mk := new(MultiLock) + var err error + mk.gen, err = NewKeyGenerator(SetShortDelayIterations()) + if err != nil { + return nil, err + } + return mk, nil +} + +func (m *MultiLock) AddPassphrase(id string, pass []byte, realPass []byte) error { + genkey, salt, err := m.gen.GenerateKey(pass) + if err != nil { + return err + } + _id := [idFieldLen]byte{} + for i := 0; i < len(id) && i < idFieldLen; i++ { + _id[i] = id[i] + } + m.id = append(m.id, _id) + m.len = append(m.len, len(pass)) + m.encryptedKey = append(m.encryptedKey, encryptedKey) + m.numKeys++ +} + +func (m *MultiLock) validate() error { + if m == nil { + return fmt.Errorf("%w: nil MultiLock", ErrInvalidHeader) + } + if m.numKeys <= 0 { + return fmt.Errorf("%w: no keys loaded in this MultiLock", ErrInvalidHeader) + } + if len(m.id) != m.numKeys { + return fmt.Errorf("%w: mismatched number of IDs", ErrInvalidHeader) + } + if len(m.len) != m.numKeys { + return fmt.Errorf("%w: mismatched number of key lengths", ErrInvalidHeader) + } + if len(m.encryptedKey) != m.numKeys { + return fmt.Errorf("%w: mismatched number of encrypted keys", ErrInvalidHeader) + } + return nil +} + +func (m *MultiLock) MarshalBinary() ([]byte, error) { + if err := m.validate(); err != nil { + return nil, err + } + var ( + buf bytes.Buffer + endian = binary.BigEndian + ) + + if err := binary.Write(&buf, endian, magicBytes); err != nil { + return nil, err + } + for i := 0; i < m.numKeys; i++ { + if err := binary.Write(&buf, endian, m.id[i]); err != nil { + return nil, err + } + if err := binary.Write(&buf, endian, m.len[i]); err != nil { + return nil, err + } + if err := binary.Write(&buf, endian, m.encryptedKey[i]); err != nil { + return nil, err + } + } + return buf.Bytes(), nil +} + +func (m *MultiLock) UnmarshalBinary(data []byte) error { + var ( + magic uint16 + num uint16 + endian binary.ByteOrder = binary.BigEndian + ) + r := bytes.NewReader(data) + if err := binary.Read(r, endian, &magic); err != nil { + return err + } + if magic == magicBytesInverse { + endian = binary.LittleEndian + } + if err := binary.Read(r, endian, &num); err != nil { + return err + } + m.numKeys = int(num) + m.id = make([][idFieldLen]byte, m.numKeys) + m.len = make([]int, m.numKeys) + m.encryptedKey = make([][]byte, m.numKeys) + + for i := 0; i < m.numKeys; i++ { + var ( + id [idFieldLen]byte + keyLen uint16 + key []byte + ) + if err := binary.Read(r, endian, &id); err != nil { + return err + } + if err := binary.Read(r, endian, &keyLen); err != nil { + return err + } + key = make([]byte, int(keyLen)) + if err := binary.Read(r, endian, &key); err != nil { + return err + } + m.id[i] = id + m.len[i] = int(keyLen) + m.encryptedKey[i] = key + } + return nil +} diff --git a/pkg/passlock/multilock_test.go b/pkg/passlock/multilock_test.go new file mode 100644 index 0000000..7232c96 --- /dev/null +++ b/pkg/passlock/multilock_test.go @@ -0,0 +1,24 @@ +package passlock + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestMultikey_AddKey(t *testing.T) { + mk, err := NewMultiLocker() + assert.NoError(t, err) + mk.AddKey("Test", []byte("test value")) + assert.Equal(t, 1, mk.numKeys) + assert.Len(t, mk.id, 1) + assert.Len(t, mk.id[0], 32) + + for i := len("Test"); i < 32; i++ { + assert.Equal(t, uint8(0), mk.id[0][i]) + } + + assert.Len(t, mk.len, 1) + assert.Len(t, mk.encryptedKey, 1) + assert.Equal(t, len("test value"), mk.len[0]) + assert.Equal(t, []byte("test value"), mk.encryptedKey[0]) +} diff --git a/pkg/passlock/multilocker.go b/pkg/passlock/multilocker.go new file mode 100644 index 0000000..6029862 --- /dev/null +++ b/pkg/passlock/multilocker.go @@ -0,0 +1,19 @@ +package passlock + +const ( + magicBytes uint16 = 0x1ff1 + magicBytesInverse uint16 = 0xf11f + idFieldLen = 32 +) + +type MultiLocker struct { + numKeys int + id [][idFieldLen]byte + len []int + encryptedKey [][]byte + payload []byte + + key []byte + multiKeyGen *KeyGenerator + keyGen *KeyGenerator +} From 1435efa0691c6241219dd4d64e50142f103e256a Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Wed, 26 Jul 2023 12:20:00 -0500 Subject: [PATCH 06/17] Adding multilocker - Using binmap to make binary read/write much easier. --- go.mod | 1 + go.sum | 2 + pkg/passlock/multilock.go | 135 ------------------ pkg/passlock/multilock_test.go | 24 ---- pkg/passlock/multilocker.go | 226 +++++++++++++++++++++++++++++-- pkg/passlock/multilocker_test.go | 34 +++++ 6 files changed, 251 insertions(+), 171 deletions(-) delete mode 100644 pkg/passlock/multilock.go delete mode 100644 pkg/passlock/multilock_test.go create mode 100644 pkg/passlock/multilocker_test.go diff --git a/go.mod b/go.mod index e9f7aa1..5444adb 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/saylorsolutions/gocryptx go 1.19 require ( + github.com/saylorsolutions/binmap v0.2.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.11.0 diff --git a/go.sum b/go.sum index a49392a..977b6c3 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/saylorsolutions/binmap v0.2.0 h1:HADBNp5OROKwpy/cdQxrLnAkoIFl4eD6g8ixzoi5vkc= +github.com/saylorsolutions/binmap v0.2.0/go.mod h1:nNL5x213T4kD+n7Oe8j0cSDXym11HX9++T3immOI1hg= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= diff --git a/pkg/passlock/multilock.go b/pkg/passlock/multilock.go deleted file mode 100644 index 6686676..0000000 --- a/pkg/passlock/multilock.go +++ /dev/null @@ -1,135 +0,0 @@ -package passlock - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" -) - -var ( - ErrInvalidHeader = errors.New("invalid MultiLock header") -) - -// MultiLock is used to allow multiple distinct passphrases to decrypt an encrypted payload. -type MultiLock struct { - numKeys int - id [][idFieldLen]byte - len []int - encryptedKey [][]byte - gen *KeyGenerator -} - -func NewMultiLocker() (*MultiLock, error) { - mk := new(MultiLock) - var err error - mk.gen, err = NewKeyGenerator(SetShortDelayIterations()) - if err != nil { - return nil, err - } - return mk, nil -} - -func (m *MultiLock) AddPassphrase(id string, pass []byte, realPass []byte) error { - genkey, salt, err := m.gen.GenerateKey(pass) - if err != nil { - return err - } - _id := [idFieldLen]byte{} - for i := 0; i < len(id) && i < idFieldLen; i++ { - _id[i] = id[i] - } - m.id = append(m.id, _id) - m.len = append(m.len, len(pass)) - m.encryptedKey = append(m.encryptedKey, encryptedKey) - m.numKeys++ -} - -func (m *MultiLock) validate() error { - if m == nil { - return fmt.Errorf("%w: nil MultiLock", ErrInvalidHeader) - } - if m.numKeys <= 0 { - return fmt.Errorf("%w: no keys loaded in this MultiLock", ErrInvalidHeader) - } - if len(m.id) != m.numKeys { - return fmt.Errorf("%w: mismatched number of IDs", ErrInvalidHeader) - } - if len(m.len) != m.numKeys { - return fmt.Errorf("%w: mismatched number of key lengths", ErrInvalidHeader) - } - if len(m.encryptedKey) != m.numKeys { - return fmt.Errorf("%w: mismatched number of encrypted keys", ErrInvalidHeader) - } - return nil -} - -func (m *MultiLock) MarshalBinary() ([]byte, error) { - if err := m.validate(); err != nil { - return nil, err - } - var ( - buf bytes.Buffer - endian = binary.BigEndian - ) - - if err := binary.Write(&buf, endian, magicBytes); err != nil { - return nil, err - } - for i := 0; i < m.numKeys; i++ { - if err := binary.Write(&buf, endian, m.id[i]); err != nil { - return nil, err - } - if err := binary.Write(&buf, endian, m.len[i]); err != nil { - return nil, err - } - if err := binary.Write(&buf, endian, m.encryptedKey[i]); err != nil { - return nil, err - } - } - return buf.Bytes(), nil -} - -func (m *MultiLock) UnmarshalBinary(data []byte) error { - var ( - magic uint16 - num uint16 - endian binary.ByteOrder = binary.BigEndian - ) - r := bytes.NewReader(data) - if err := binary.Read(r, endian, &magic); err != nil { - return err - } - if magic == magicBytesInverse { - endian = binary.LittleEndian - } - if err := binary.Read(r, endian, &num); err != nil { - return err - } - m.numKeys = int(num) - m.id = make([][idFieldLen]byte, m.numKeys) - m.len = make([]int, m.numKeys) - m.encryptedKey = make([][]byte, m.numKeys) - - for i := 0; i < m.numKeys; i++ { - var ( - id [idFieldLen]byte - keyLen uint16 - key []byte - ) - if err := binary.Read(r, endian, &id); err != nil { - return err - } - if err := binary.Read(r, endian, &keyLen); err != nil { - return err - } - key = make([]byte, int(keyLen)) - if err := binary.Read(r, endian, &key); err != nil { - return err - } - m.id[i] = id - m.len[i] = int(keyLen) - m.encryptedKey[i] = key - } - return nil -} diff --git a/pkg/passlock/multilock_test.go b/pkg/passlock/multilock_test.go deleted file mode 100644 index 7232c96..0000000 --- a/pkg/passlock/multilock_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package passlock - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestMultikey_AddKey(t *testing.T) { - mk, err := NewMultiLocker() - assert.NoError(t, err) - mk.AddKey("Test", []byte("test value")) - assert.Equal(t, 1, mk.numKeys) - assert.Len(t, mk.id, 1) - assert.Len(t, mk.id[0], 32) - - for i := len("Test"); i < 32; i++ { - assert.Equal(t, uint8(0), mk.id[0][i]) - } - - assert.Len(t, mk.len, 1) - assert.Len(t, mk.encryptedKey, 1) - assert.Equal(t, len("test value"), mk.len[0]) - assert.Equal(t, []byte("test value"), mk.encryptedKey[0]) -} diff --git a/pkg/passlock/multilocker.go b/pkg/passlock/multilocker.go index 6029862..677355d 100644 --- a/pkg/passlock/multilocker.go +++ b/pkg/passlock/multilocker.go @@ -1,19 +1,221 @@ package passlock +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + bin "github.com/saylorsolutions/binmap" + "io" + "sort" +) + const ( - magicBytes uint16 = 0x1ff1 - magicBytesInverse uint16 = 0xf11f - idFieldLen = 32 + idFieldLen = 32 ) +var ( + ErrInvalidHeader = errors.New("invalid MultiLocker header") +) + +type MultiKey struct { + id string + encryptedKey []byte +} + +func (k *MultiKey) mapper() bin.Mapper { + return bin.MapSequence( + bin.FixedString(&k.id, idFieldLen), + bin.DynamicSlice(&k.encryptedKey, bin.Byte), + ) +} + type MultiLocker struct { - numKeys int - id [][idFieldLen]byte - len []int - encryptedKey [][]byte - payload []byte - - key []byte - multiKeyGen *KeyGenerator - keyGen *KeyGenerator + mkeys []MultiKey + payload []byte + + baseKey []byte + keyGen *KeyGenerator +} + +func NewMultiLocker(gen *KeyGenerator) *MultiLocker { + return &MultiLocker{ + keyGen: gen, + } +} + +func (l *MultiLocker) validate() error { + if len(l.payload) == 0 { + return errors.New("no payload set for update") + } + if l.keyGen == nil { + return errors.New("no generator set") + } + return nil +} + +func (l *MultiLocker) mapper() bin.Mapper { + return bin.MapSequence(bin.DynamicSlice(&l.mkeys, func(k *MultiKey) bin.Mapper { + return k.mapper() + }), bin.Any(&l.payload, func(r io.Reader, endian binary.ByteOrder) error { + payload, err := io.ReadAll(r) + if err != nil { + return err + } + l.payload = payload + return nil + }, func(w io.Writer, endian binary.ByteOrder) error { + _, err := io.Copy(w, bytes.NewReader(l.payload)) + return err + })) +} + +func (l *MultiLocker) Read(r io.Reader) error { + if err := l.mapper().Read(r, binary.BigEndian); err != nil { + return fmt.Errorf("%w: %v", ErrInvalidHeader, err) + } + return nil +} + +func (l *MultiLocker) Write(w io.Writer) error { + if len(l.mkeys) == 0 { + return errors.New("refusing to write MultiLocker without keys, data will be unrecoverable") + } + return l.mapper().Write(w, binary.BigEndian) +} + +// EnableUpdate validates the MultiLocker and ensures that it's in a suitable state for updating. +// The original payload password must be used (not MultiKey passwords) to validate that the correct key is populated. +// The first validation error will be returned. +func (l *MultiLocker) EnableUpdate(pass []byte) error { + err := l.validate() + if err != nil { + return err + } + derivedKey, err := l.keyGen.DeriveKey(pass, l.payload) + if err != nil { + return fmt.Errorf("failed to derive baseKey from payload: %w", err) + } + _, err = Unlock(derivedKey, l.payload) + if err != nil { + return fmt.Errorf("invalid base key: %w", err) + } + l.baseKey = derivedKey + return nil +} + +func (l *MultiLocker) ListKeyIDs() []string { + ids := make([]string, len(l.mkeys)) + for i, mk := range l.mkeys { + ids[i] = mk.id + } + sort.Strings(ids) + return ids +} + +func (l *MultiLocker) AddPass(id string, pass []byte) error { + if len(id) > idFieldLen { + return fmt.Errorf("id value is greater than the maximum field width of %d", idFieldLen) + } + if len(l.baseKey) == 0 { + return errors.New("payload baseKey is not populated, prepare the MultiLocker for update first") + } + if err := l.validate(); err != nil { + return err + } + newPassKey, salt, err := l.keyGen.GenerateKey(pass) + if err != nil { + return err + } + encryptedKey, err := Lock(newPassKey, salt, l.baseKey) + if err != nil { + return err + } + newKey := MultiKey{ + id: id, + encryptedKey: encryptedKey, + } + l.mkeys = append(l.mkeys, newKey) + return nil +} + +func (l *MultiLocker) RemovePass(id string) { + for i := 0; i < len(l.mkeys); i++ { + if l.mkeys[i].id == id { + l.mkeys = append(l.mkeys[:i], l.mkeys[i+1:]...) + return + } + } +} + +func (l *MultiLocker) UpdatePass(id string, pass []byte) error { + if len(id) > idFieldLen { + return fmt.Errorf("id value is greater than the maximum field width of %d", idFieldLen) + } + if len(l.baseKey) == 0 { + return errors.New("payload baseKey is not populated, prepare the MultiLocker for update first") + } + if err := l.validate(); err != nil { + return err + } + newPassKey, salt, err := l.keyGen.GenerateKey(pass) + if err != nil { + return err + } + encryptedKey, err := Lock(newPassKey, salt, l.baseKey) + if err != nil { + return err + } + for _, mk := range l.mkeys { + if mk.id == id { + mk.encryptedKey = encryptedKey + return nil + } + } + return errors.New("given ID is not present in this MultiLocker") +} + +func (l *MultiLocker) Lock(pass []byte, unencrypted []byte) error { + if l.keyGen == nil { + return errors.New("missing key generator") + } + if len(l.mkeys) > 0 { + return errors.New("locking a new payload will invalidate all existing MultiKeys") + } + key, salt, err := l.keyGen.GenerateKey(pass) + if err != nil { + return err + } + encrypted, err := Lock(key, salt, unencrypted) + if err != nil { + return err + } + l.payload = encrypted + l.baseKey = key + return nil +} + +func (l *MultiLocker) Unlock(id string, pass []byte) ([]byte, error) { + if err := l.validate(); err != nil { + return nil, err + } + for _, mk := range l.mkeys { + if mk.id == id { + passKey, err := l.keyGen.DeriveKey(pass, mk.encryptedKey) + if err != nil { + return nil, err + } + baseKey, err := Unlock(passKey, mk.encryptedKey) + if err != nil { + return nil, errors.New("invalid password") + } + data, err := Unlock(baseKey, l.payload) + baseKey = nil + if err != nil { + return nil, fmt.Errorf("failed to decrypt payload: %w", err) + } + return data, nil + } + } + return nil, errors.New("multikey ID not found") } diff --git a/pkg/passlock/multilocker_test.go b/pkg/passlock/multilocker_test.go new file mode 100644 index 0000000..176a431 --- /dev/null +++ b/pkg/passlock/multilocker_test.go @@ -0,0 +1,34 @@ +package passlock + +import ( + "bytes" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestMultikey_AddKey(t *testing.T) { + var ( + plaintext = "A secret message" + basePass = "passphrase" + buf bytes.Buffer + ) + gen, err := NewKeyGenerator(SetShortDelayIterations()) + assert.NoError(t, err) + mk := NewMultiLocker(gen) + assert.NoError(t, mk.Lock([]byte(basePass), []byte(plaintext))) + + assert.NoError(t, mk.AddPass("developer", []byte("s3cre+"))) + assert.NoError(t, mk.AddPass("other", []byte("some other secret"))) + assert.NoError(t, mk.Write(&buf)) + + mkdata := buf.Bytes() + buf.Reset() + buf.Write(mkdata) + + mk = NewMultiLocker(gen) + assert.NoError(t, mk.Read(&buf)) + assert.Len(t, mk.mkeys, 2) + data, err := mk.Unlock("developer", []byte("s3cre+")) + assert.NoError(t, err) + assert.Equal(t, plaintext, string(data)) +} From f907e325ec9c75440a2ad42d6fff68f8972c52c0 Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Wed, 26 Jul 2023 12:47:33 -0500 Subject: [PATCH 07/17] Adding better validation and error reporting - Making RemovePass require knowledge of the baseKey. - Added DisableUpdate method. --- pkg/passlock/multilocker.go | 51 ++++++++++++++++++++------------ pkg/passlock/multilocker_test.go | 20 +++++++++++++ 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/pkg/passlock/multilocker.go b/pkg/passlock/multilocker.go index 677355d..2f9fb88 100644 --- a/pkg/passlock/multilocker.go +++ b/pkg/passlock/multilocker.go @@ -15,7 +15,8 @@ const ( ) var ( - ErrInvalidHeader = errors.New("invalid MultiLocker header") + ErrInvalidHeader = errors.New("invalid MultiLocker header") + ErrInvalidPassword = errors.New("invalid password") ) type MultiKey struct { @@ -44,7 +45,7 @@ func NewMultiLocker(gen *KeyGenerator) *MultiLocker { } } -func (l *MultiLocker) validate() error { +func (l *MultiLocker) validateInitialized() error { if len(l.payload) == 0 { return errors.New("no payload set for update") } @@ -54,6 +55,16 @@ func (l *MultiLocker) validate() error { return nil } +func (l *MultiLocker) validateForUpdate() error { + if err := l.validateInitialized(); err != nil { + return err + } + if len(l.baseKey) == 0 { + return errors.New("payload baseKey is not populated, enable update on this MultiLocker first") + } + return nil +} + func (l *MultiLocker) mapper() bin.Mapper { return bin.MapSequence(bin.DynamicSlice(&l.mkeys, func(k *MultiKey) bin.Mapper { return k.mapper() @@ -88,7 +99,7 @@ func (l *MultiLocker) Write(w io.Writer) error { // The original payload password must be used (not MultiKey passwords) to validate that the correct key is populated. // The first validation error will be returned. func (l *MultiLocker) EnableUpdate(pass []byte) error { - err := l.validate() + err := l.validateInitialized() if err != nil { return err } @@ -98,12 +109,16 @@ func (l *MultiLocker) EnableUpdate(pass []byte) error { } _, err = Unlock(derivedKey, l.payload) if err != nil { - return fmt.Errorf("invalid base key: %w", err) + return fmt.Errorf("%w: invalid base pass", ErrInvalidPassword) } l.baseKey = derivedKey return nil } +func (l *MultiLocker) DisableUpdate() { + l.baseKey = nil +} + func (l *MultiLocker) ListKeyIDs() []string { ids := make([]string, len(l.mkeys)) for i, mk := range l.mkeys { @@ -114,13 +129,10 @@ func (l *MultiLocker) ListKeyIDs() []string { } func (l *MultiLocker) AddPass(id string, pass []byte) error { - if len(id) > idFieldLen { - return fmt.Errorf("id value is greater than the maximum field width of %d", idFieldLen) - } - if len(l.baseKey) == 0 { - return errors.New("payload baseKey is not populated, prepare the MultiLocker for update first") + if len(id) > idFieldLen || len(id) == 0 { + return fmt.Errorf("id value is not within the valid range of 1-%d bytes", idFieldLen) } - if err := l.validate(); err != nil { + if err := l.validateForUpdate(); err != nil { return err } newPassKey, salt, err := l.keyGen.GenerateKey(pass) @@ -139,23 +151,24 @@ func (l *MultiLocker) AddPass(id string, pass []byte) error { return nil } -func (l *MultiLocker) RemovePass(id string) { +func (l *MultiLocker) RemovePass(id string) error { + if err := l.validateForUpdate(); err != nil { + return err + } for i := 0; i < len(l.mkeys); i++ { if l.mkeys[i].id == id { l.mkeys = append(l.mkeys[:i], l.mkeys[i+1:]...) - return + return nil } } + return nil } func (l *MultiLocker) UpdatePass(id string, pass []byte) error { if len(id) > idFieldLen { return fmt.Errorf("id value is greater than the maximum field width of %d", idFieldLen) } - if len(l.baseKey) == 0 { - return errors.New("payload baseKey is not populated, prepare the MultiLocker for update first") - } - if err := l.validate(); err != nil { + if err := l.validateForUpdate(); err != nil { return err } newPassKey, salt, err := l.keyGen.GenerateKey(pass) @@ -196,7 +209,7 @@ func (l *MultiLocker) Lock(pass []byte, unencrypted []byte) error { } func (l *MultiLocker) Unlock(id string, pass []byte) ([]byte, error) { - if err := l.validate(); err != nil { + if err := l.validateInitialized(); err != nil { return nil, err } for _, mk := range l.mkeys { @@ -207,12 +220,12 @@ func (l *MultiLocker) Unlock(id string, pass []byte) ([]byte, error) { } baseKey, err := Unlock(passKey, mk.encryptedKey) if err != nil { - return nil, errors.New("invalid password") + return nil, ErrInvalidPassword } data, err := Unlock(baseKey, l.payload) baseKey = nil if err != nil { - return nil, fmt.Errorf("failed to decrypt payload: %w", err) + return nil, fmt.Errorf("%w: invalid base pass", ErrInvalidPassword) } return data, nil } diff --git a/pkg/passlock/multilocker_test.go b/pkg/passlock/multilocker_test.go index 176a431..87a0cb3 100644 --- a/pkg/passlock/multilocker_test.go +++ b/pkg/passlock/multilocker_test.go @@ -32,3 +32,23 @@ func TestMultikey_AddKey(t *testing.T) { assert.NoError(t, err) assert.Equal(t, plaintext, string(data)) } + +func TestMultiLocker_RemovePass(t *testing.T) { + var ( + plaintext = "A secret message" + basePass = "passphrase" + ) + gen, err := NewKeyGenerator(SetShortDelayIterations()) + assert.NoError(t, err) + mk := NewMultiLocker(gen) + assert.NoError(t, mk.Lock([]byte(basePass), []byte(plaintext))) + + assert.NoError(t, mk.AddPass("developer", []byte("s3cre+"))) + assert.NoError(t, mk.AddPass("other", []byte("some other secret"))) + + mk.DisableUpdate() + assert.Error(t, mk.RemovePass("other"), "Should return an error when update is not enabled.") + + assert.NoError(t, mk.EnableUpdate([]byte(basePass))) + assert.NoError(t, mk.RemovePass("other"), "Should be okay to update.") +} From 9cca0ab057126fbcd7a75b0065e27c3a53dddeac Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Wed, 26 Jul 2023 13:49:13 -0500 Subject: [PATCH 08/17] Persisting KeyGenerator in MultiLocker --- pkg/passlock/key.go | 42 +++++++++++++++++++++++-------------- pkg/passlock/key_test.go | 32 ++++++++++++++++++++++++---- pkg/passlock/multilocker.go | 28 ++++++++++++++----------- 3 files changed, 70 insertions(+), 32 deletions(-) diff --git a/pkg/passlock/key.go b/pkg/passlock/key.go index c7815f7..43f4a72 100644 --- a/pkg/passlock/key.go +++ b/pkg/passlock/key.go @@ -4,15 +4,16 @@ import ( "crypto/rand" "errors" "fmt" + bin "github.com/saylorsolutions/binmap" "golang.org/x/crypto/scrypt" ) const ( - DefaultLargeIterations = 1_048_576 - DefaultInteractiveIterations = 131_072 - DefaultRelBlockSize = 8 - DefaultCpuCost = 1 - AES256KeySize = 256 / 8 + DefaultLargeIterations uint32 = 1_048_576 + DefaultInteractiveIterations uint32 = 131_072 + DefaultRelBlockSize uint8 = 8 + DefaultCpuCost uint8 = 1 + AES256KeySize uint8 = 256 / 8 ) var ( @@ -21,10 +22,19 @@ var ( ) type KeyGenerator struct { - iterations int - relativeBlockSize int - cpuCost int - aesKeySize int + iterations uint32 + relativeBlockSize uint8 + cpuCost uint8 + aesKeySize uint8 +} + +func (g *KeyGenerator) mapper() bin.Mapper { + return bin.MapSequence( + bin.Int(&g.iterations), + bin.Byte(&g.relativeBlockSize), + bin.Byte(&g.cpuCost), + bin.Byte(&g.aesKeySize), + ) } type GeneratorOpt = func(*KeyGenerator) error @@ -56,7 +66,7 @@ func SetShortDelayIterations() GeneratorOpt { // SetIterations allows the caller to customize the iteration count. // Only use this option if you know what you're doing. -func SetIterations(iterations int) GeneratorOpt { +func SetIterations(iterations uint32) GeneratorOpt { return func(gen *KeyGenerator) error { if iterations <= 1 { return errors.New("iterations cannot be <= 1") @@ -71,7 +81,7 @@ func SetIterations(iterations int) GeneratorOpt { // SetCPUCost sets the parallelism factor for key generation from the default of 1. // Only use this option if you know what you're doing. -func SetCPUCost(cost int) GeneratorOpt { +func SetCPUCost(cost uint8) GeneratorOpt { return func(gen *KeyGenerator) error { if cost < DefaultCpuCost { return errors.New("cpu cost must be at least 1") @@ -83,7 +93,7 @@ func SetCPUCost(cost int) GeneratorOpt { // SetRelativeBlockSize sets the relative block size. // Only use this option if you know what you're doing. -func SetRelativeBlockSize(size int) GeneratorOpt { +func SetRelativeBlockSize(size uint8) GeneratorOpt { return func(gen *KeyGenerator) error { if size < DefaultRelBlockSize { return errors.New("relative block size must be at least 8") @@ -120,7 +130,7 @@ func (g *KeyGenerator) GenerateKey(pass []byte) (key, salt []byte, err error) { if _, err = rand.Read(salt); err != nil { return nil, nil, err } - key, err = scrypt.Key(pass, salt, g.iterations, g.relativeBlockSize, g.cpuCost, g.aesKeySize) + key, err = scrypt.Key(pass, salt, int(g.iterations), int(g.relativeBlockSize), int(g.cpuCost), int(g.aesKeySize)) return key, salt, err } @@ -129,12 +139,12 @@ func (g *KeyGenerator) DeriveKey(pass, data []byte) (key []byte, err error) { if len(pass) == 0 { return nil, ErrEmptyPassPhrase } - if len(data) <= g.aesKeySize { + if len(data) <= int(g.aesKeySize) { return nil, fmt.Errorf("%w: input data isn't long enough to contain a key salt", ErrInvalidData) } var salt []byte - salt = data[len(data)-g.aesKeySize:] - key, err = scrypt.Key(pass, salt, g.iterations, g.relativeBlockSize, g.cpuCost, g.aesKeySize) + salt = data[len(data)-int(g.aesKeySize):] + key, err = scrypt.Key(pass, salt, int(g.iterations), int(g.relativeBlockSize), int(g.cpuCost), int(g.aesKeySize)) if err != nil { return nil, err } diff --git a/pkg/passlock/key_test.go b/pkg/passlock/key_test.go index 52bde67..ab8a3f7 100644 --- a/pkg/passlock/key_test.go +++ b/pkg/passlock/key_test.go @@ -1,6 +1,8 @@ package passlock import ( + "bytes" + "encoding/binary" "github.com/stretchr/testify/assert" "testing" ) @@ -16,8 +18,8 @@ func TestNewKeyGenerator(t *testing.T) { key, salt, err := gen.GenerateKey([]byte("a test password")) assert.NoError(t, err) - assert.Len(t, key, gen.aesKeySize) - assert.Len(t, salt, gen.aesKeySize) + assert.Len(t, key, int(gen.aesKeySize)) + assert.Len(t, salt, int(gen.aesKeySize)) } func TestNewKeyGenerator_Custom(t *testing.T) { @@ -38,6 +40,28 @@ func TestNewKeyGenerator_Custom(t *testing.T) { key, salt, err := gen.GenerateKey([]byte("a test password")) assert.NoError(t, err) - assert.Len(t, key, gen.aesKeySize) - assert.Len(t, salt, gen.aesKeySize) + assert.Len(t, key, int(gen.aesKeySize)) + assert.Len(t, salt, int(gen.aesKeySize)) +} + +func TestKeyGenerator_mapper(t *testing.T) { + var ( + buf bytes.Buffer + ) + gen, err := NewKeyGenerator(SetShortDelayIterations()) + assert.NoError(t, err) + assert.NotNil(t, gen) + + assert.NoError(t, gen.mapper().Write(&buf, binary.BigEndian)) + updated, err := NewKeyGenerator( + SetIterations(1<<4), + SetCPUCost(4), + SetRelativeBlockSize(128), + ) + assert.NoError(t, err) + assert.NoError(t, updated.mapper().Read(&buf, binary.BigEndian)) + assert.Equal(t, DefaultInteractiveIterations, updated.iterations) + assert.Equal(t, DefaultCpuCost, updated.cpuCost) + assert.Equal(t, DefaultRelBlockSize, updated.relativeBlockSize) + assert.Equal(t, AES256KeySize, updated.aesKeySize) } diff --git a/pkg/passlock/multilocker.go b/pkg/passlock/multilocker.go index 2f9fb88..ea0ac0e 100644 --- a/pkg/passlock/multilocker.go +++ b/pkg/passlock/multilocker.go @@ -66,19 +66,23 @@ func (l *MultiLocker) validateForUpdate() error { } func (l *MultiLocker) mapper() bin.Mapper { - return bin.MapSequence(bin.DynamicSlice(&l.mkeys, func(k *MultiKey) bin.Mapper { - return k.mapper() - }), bin.Any(&l.payload, func(r io.Reader, endian binary.ByteOrder) error { - payload, err := io.ReadAll(r) - if err != nil { + return bin.MapSequence( + bin.DynamicSlice(&l.mkeys, func(k *MultiKey) bin.Mapper { + return k.mapper() + }), + l.keyGen.mapper(), + bin.Any(&l.payload, func(r io.Reader, endian binary.ByteOrder) error { + payload, err := io.ReadAll(r) + if err != nil { + return err + } + l.payload = payload + return nil + }, func(w io.Writer, endian binary.ByteOrder) error { + _, err := io.Copy(w, bytes.NewReader(l.payload)) return err - } - l.payload = payload - return nil - }, func(w io.Writer, endian binary.ByteOrder) error { - _, err := io.Copy(w, bytes.NewReader(l.payload)) - return err - })) + }), + ) } func (l *MultiLocker) Read(r io.Reader) error { From 734fcd24421c6f293fffcaae16d923a31db15cfe Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Wed, 26 Jul 2023 13:52:57 -0500 Subject: [PATCH 09/17] Making iteration count uint64 to allow for growth since the large iterations setting is already close to the type's capacity --- pkg/passlock/key.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/passlock/key.go b/pkg/passlock/key.go index 43f4a72..66456bb 100644 --- a/pkg/passlock/key.go +++ b/pkg/passlock/key.go @@ -9,8 +9,8 @@ import ( ) const ( - DefaultLargeIterations uint32 = 1_048_576 - DefaultInteractiveIterations uint32 = 131_072 + DefaultLargeIterations uint64 = 1 << 30 + DefaultInteractiveIterations uint64 = 1 << 17 DefaultRelBlockSize uint8 = 8 DefaultCpuCost uint8 = 1 AES256KeySize uint8 = 256 / 8 @@ -22,7 +22,7 @@ var ( ) type KeyGenerator struct { - iterations uint32 + iterations uint64 relativeBlockSize uint8 cpuCost uint8 aesKeySize uint8 @@ -66,7 +66,7 @@ func SetShortDelayIterations() GeneratorOpt { // SetIterations allows the caller to customize the iteration count. // Only use this option if you know what you're doing. -func SetIterations(iterations uint32) GeneratorOpt { +func SetIterations(iterations uint64) GeneratorOpt { return func(gen *KeyGenerator) error { if iterations <= 1 { return errors.New("iterations cannot be <= 1") From 2a8ac0a3565bfd7eb20f2a1b1bfe3e438da2c131 Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Wed, 26 Jul 2023 16:20:50 -0500 Subject: [PATCH 10/17] Updating docs and adding a few nice features - Allowing for MultiLocker to Lock a new payload without invalidating surrogate keys. - Enabling AES-128. - Adding a convenience method to KeyGenerator to return both a derived key and salt. Used for MultiLocker re-locking. - Removing write without surrogate keys constraint, since EnableUpdate can still succeed without them. --- README.md | 5 +++ pkg/passlock/doc.go | 16 ++++++++-- pkg/passlock/key.go | 25 ++++++++++++--- pkg/passlock/key_test.go | 1 + pkg/passlock/locker_test.go | 26 ++++++++++++++++ pkg/passlock/multilocker.go | 53 +++++++++++++++++++------------- pkg/passlock/multilocker_test.go | 27 +++++++++++++++- 7 files changed, 124 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 17e7c75..4dfa667 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,11 @@ If this is the case - and even before then - feel free to fork this repository a ## Packages * **xor:** Provides some utilities for XOR screening, including an io.Reader and io.Writer implementation that screens in flight. +* **passlock:** Provides some utilities for AES 128/256 encryption using a user-supplied passphrase. This is useful for situations where key management is considered harder than password management. + * Provides a KeyGenerator type that uses scrypt under the hood to generate AES 128/256 keys based on the given passphrase and a secure random seed. + * The key generator may be tuned to match your threat model, but reasonable default are provided. + * This also includes a way to encrypt/decrypt a payload with multiple, surrogate keys. This allows multiple, independent passphrases to be used to interact with a payload. + * There are no guarantees that this mechanism is interoperable with other passphrase locking mechanisms or systems. ## Applications * **xorgen:** Provides a CLI that can be used with go:generate comments to easily embed XOR screened and compressed files. diff --git a/pkg/passlock/doc.go b/pkg/passlock/doc.go index 7f3ee2f..546de62 100644 --- a/pkg/passlock/doc.go +++ b/pkg/passlock/doc.go @@ -11,11 +11,23 @@ The key, salt, and plaintext are passed to the Lock function to encrypt the payl The key is recovered from the encrypted payload by passing the original passphrase and the payload to KeyGenerator.Derive. The key and encrypted payload are passed to the Unlock function to decrypt the payload and return the original plain text. +The MultiLocker type extends the functionality above by providing the ability to use multiple surrogate keys to interact with the encrypted payload. +A MultiLocker is created using a KeyGenerator, and the encrypted payload is set by calling MultiLocker.Lock with the base passphrase and plaintext. +Once created, surrogate keys may be added to the MultiLocker that allow reading the encrypted payload. +A MultiLocker with surrogate keys and encrypted payload may be persisted to disk in binary form, and read back - including key generation settings. + +A freshly read MultiLocker may not be changed in any way. Editing is enabled by calling EnableUpdate with the base passphrase. +After this call completes successfully, surrogate keys may be added or removed. +A new encrypted payload may not be set to a MultiLocker if surrogate keys exist, and the MultiLocker has not had EnableUpdate called. Allowing this operation would invalidate those keys otherwise. + # General guidelines: - It's possible to customize the CPU cost, iteration count, and relative block size parameters directly for key generation. If you're not an expert, then don't use SetIterations, SetCPUCost, or SetRelativeBlockSize. - Both short and long delay iteration GeneratorOpt functions are provided, choose the correct iterations for your use-case using either SetLongDelayIterations or SetShortDelayIterations. - - This method of encryption (AES256GCM) supports encrypting and authenticating at most about 64GB at a time. You could get around this by splitting a very large file into multiple chunks that include some metadata to prevent reordering or truncating. - - AES-256 is a good default for a lot of cases, with excellent security and good throughput speeds. This library only supports AES-256 since that is the best supported by the Go standard lib, and it conforms to the constraints posed by AES in general. + - This method of encryption (AES-GCM) supports encrypting and authenticating at most about 64GB at a time. You could get around this by splitting a very large file into multiple chunks that include some metadata to prevent reordering or truncating. + - AES-256 is a good default for a lot of cases, with excellent security and good throughput speeds. + - This library supports AES-256 since that is the best supported by the Go standard lib, but AES-128 may also be used for situations where more throughput is desired. + - The main limit to throughput comes from key generation, the AES key size makes a much smaller impact to performance. - When deriving the key from an encrypted payload, make sure that the same KeyGenerator settings are used. Not doing so will likely result in an incorrect key. + - Technically, a surrogate key could be used to update a MultiLocker encrypted payload without invalidating other surrogate keys, since there are no cryptographic blockers to that. The base MultiLocker doesn't provide that function as a logical constraint only. */ package passlock diff --git a/pkg/passlock/key.go b/pkg/passlock/key.go index 66456bb..39e1c2b 100644 --- a/pkg/passlock/key.go +++ b/pkg/passlock/key.go @@ -14,6 +14,7 @@ const ( DefaultRelBlockSize uint8 = 8 DefaultCpuCost uint8 = 1 AES256KeySize uint8 = 256 / 8 + AES128KeySize uint8 = 128 / 8 ) var ( @@ -46,6 +47,13 @@ func SetAES256KeySize() GeneratorOpt { } } +func SetAES128KeySize() GeneratorOpt { + return func(gen *KeyGenerator) error { + gen.aesKeySize = AES128KeySize + return nil + } +} + // SetLongDelayIterations sets a higher iteration count. This is sufficient for infrequent key derivation, or cases where the key will be cached for long periods of time. // This option is much more resistant to password cracking, and is the default. func SetLongDelayIterations() GeneratorOpt { @@ -135,18 +143,25 @@ func (g *KeyGenerator) GenerateKey(pass []byte) (key, salt []byte, err error) { } // DeriveKey will recover a key with the salt in the payload and the given passphrase. +// This doesn't ensure that the given passphrase is the *correct* passphrase used to encrypt the payload. func (g *KeyGenerator) DeriveKey(pass, data []byte) (key []byte, err error) { + key, _, err = g.DeriveKeySalt(pass, data) + return key, err +} + +// DeriveKeySalt will recover a key and the original salt in the payload with the given passphrase. +// This doesn't ensure that the given passphrase is the *correct* passphrase used to encrypt the payload. +func (g *KeyGenerator) DeriveKeySalt(pass, data []byte) (key []byte, salt []byte, err error) { if len(pass) == 0 { - return nil, ErrEmptyPassPhrase + return nil, nil, ErrEmptyPassPhrase } if len(data) <= int(g.aesKeySize) { - return nil, fmt.Errorf("%w: input data isn't long enough to contain a key salt", ErrInvalidData) + return nil, nil, fmt.Errorf("%w: input data isn't long enough to contain a key salt", ErrInvalidData) } - var salt []byte salt = data[len(data)-int(g.aesKeySize):] key, err = scrypt.Key(pass, salt, int(g.iterations), int(g.relativeBlockSize), int(g.cpuCost), int(g.aesKeySize)) if err != nil { - return nil, err + return nil, nil, err } - return key, nil + return key, salt, nil } diff --git a/pkg/passlock/key_test.go b/pkg/passlock/key_test.go index ab8a3f7..a4e72df 100644 --- a/pkg/passlock/key_test.go +++ b/pkg/passlock/key_test.go @@ -57,6 +57,7 @@ func TestKeyGenerator_mapper(t *testing.T) { SetIterations(1<<4), SetCPUCost(4), SetRelativeBlockSize(128), + SetAES128KeySize(), ) assert.NoError(t, err) assert.NoError(t, updated.mapper().Read(&buf, binary.BigEndian)) diff --git a/pkg/passlock/locker_test.go b/pkg/passlock/locker_test.go index 4ccc28c..90418f9 100644 --- a/pkg/passlock/locker_test.go +++ b/pkg/passlock/locker_test.go @@ -30,3 +30,29 @@ func TestLockUnlock(t *testing.T) { assert.NotEqual(t, encrypted, unencrypted) assert.Equal(t, data, string(unencrypted)) } + +func TestLockUnlock128(t *testing.T) { + const password = "password" + const data = "How wonderful life is while you're in the world" + dataBytes := []byte(data) + + gen, err := NewKeyGenerator(SetShortDelayIterations(), SetAES128KeySize()) + assert.NoError(t, err) + key, salt, err := gen.GenerateKey([]byte(password)) + assert.NoError(t, err) + + encrypted, err := Lock(key, salt, dataBytes) + t.Log(string(encrypted)) + assert.NoError(t, err) + assert.NotEqual(t, dataBytes, encrypted) + + key2, err := gen.DeriveKey([]byte(password), encrypted) + assert.NoError(t, err) + assert.Equal(t, key, key2) + + unencrypted, err := Unlock(key2, encrypted) + t.Log(string(unencrypted)) + assert.NoError(t, err) + assert.NotEqual(t, encrypted, unencrypted) + assert.Equal(t, data, string(unencrypted)) +} diff --git a/pkg/passlock/multilocker.go b/pkg/passlock/multilocker.go index ea0ac0e..384ebdb 100644 --- a/pkg/passlock/multilocker.go +++ b/pkg/passlock/multilocker.go @@ -19,12 +19,12 @@ var ( ErrInvalidPassword = errors.New("invalid password") ) -type MultiKey struct { +type surrogateKey struct { id string encryptedKey []byte } -func (k *MultiKey) mapper() bin.Mapper { +func (k *surrogateKey) mapper() bin.Mapper { return bin.MapSequence( bin.FixedString(&k.id, idFieldLen), bin.DynamicSlice(&k.encryptedKey, bin.Byte), @@ -32,7 +32,7 @@ func (k *MultiKey) mapper() bin.Mapper { } type MultiLocker struct { - mkeys []MultiKey + surKeys []surrogateKey payload []byte baseKey []byte @@ -67,7 +67,7 @@ func (l *MultiLocker) validateForUpdate() error { func (l *MultiLocker) mapper() bin.Mapper { return bin.MapSequence( - bin.DynamicSlice(&l.mkeys, func(k *MultiKey) bin.Mapper { + bin.DynamicSlice(&l.surKeys, func(k *surrogateKey) bin.Mapper { return k.mapper() }), l.keyGen.mapper(), @@ -93,15 +93,11 @@ func (l *MultiLocker) Read(r io.Reader) error { } func (l *MultiLocker) Write(w io.Writer) error { - if len(l.mkeys) == 0 { - return errors.New("refusing to write MultiLocker without keys, data will be unrecoverable") - } return l.mapper().Write(w, binary.BigEndian) } -// EnableUpdate validates the MultiLocker and ensures that it's in a suitable state for updating. -// The original payload password must be used (not MultiKey passwords) to validate that the correct key is populated. -// The first validation error will be returned. +// EnableUpdate validates the MultiLocker and ensures that it's in a suitable state for updating by setting the base key. +// The original payload passphrase must be used, not a surrogate passphrase, to validate that the correct key is populated. func (l *MultiLocker) EnableUpdate(pass []byte) error { err := l.validateInitialized() if err != nil { @@ -124,8 +120,8 @@ func (l *MultiLocker) DisableUpdate() { } func (l *MultiLocker) ListKeyIDs() []string { - ids := make([]string, len(l.mkeys)) - for i, mk := range l.mkeys { + ids := make([]string, len(l.surKeys)) + for i, mk := range l.surKeys { ids[i] = mk.id } sort.Strings(ids) @@ -147,11 +143,11 @@ func (l *MultiLocker) AddPass(id string, pass []byte) error { if err != nil { return err } - newKey := MultiKey{ + newKey := surrogateKey{ id: id, encryptedKey: encryptedKey, } - l.mkeys = append(l.mkeys, newKey) + l.surKeys = append(l.surKeys, newKey) return nil } @@ -159,9 +155,9 @@ func (l *MultiLocker) RemovePass(id string) error { if err := l.validateForUpdate(); err != nil { return err } - for i := 0; i < len(l.mkeys); i++ { - if l.mkeys[i].id == id { - l.mkeys = append(l.mkeys[:i], l.mkeys[i+1:]...) + for i := 0; i < len(l.surKeys); i++ { + if l.surKeys[i].id == id { + l.surKeys = append(l.surKeys[:i], l.surKeys[i+1:]...) return nil } } @@ -183,7 +179,7 @@ func (l *MultiLocker) UpdatePass(id string, pass []byte) error { if err != nil { return err } - for _, mk := range l.mkeys { + for _, mk := range l.surKeys { if mk.id == id { mk.encryptedKey = encryptedKey return nil @@ -196,8 +192,23 @@ func (l *MultiLocker) Lock(pass []byte, unencrypted []byte) error { if l.keyGen == nil { return errors.New("missing key generator") } - if len(l.mkeys) > 0 { - return errors.New("locking a new payload will invalidate all existing MultiKeys") + if len(l.surKeys) > 0 { + if err := l.validateForUpdate(); err != nil { + return fmt.Errorf("cannot Lock a new payload with surrogate keys until update is enabled: %w", err) + } + key, salt, err := l.keyGen.DeriveKeySalt(pass, l.payload) + if err != nil { + return err + } + if !bytes.Equal(key, l.baseKey) { + return fmt.Errorf("%w: passphrase doesn't match base passphrase", ErrInvalidPassword) + } + newPayload, err := Lock(l.baseKey, salt, unencrypted) + if err != nil { + return err + } + l.payload = newPayload + return nil } key, salt, err := l.keyGen.GenerateKey(pass) if err != nil { @@ -216,7 +227,7 @@ func (l *MultiLocker) Unlock(id string, pass []byte) ([]byte, error) { if err := l.validateInitialized(); err != nil { return nil, err } - for _, mk := range l.mkeys { + for _, mk := range l.surKeys { if mk.id == id { passKey, err := l.keyGen.DeriveKey(pass, mk.encryptedKey) if err != nil { diff --git a/pkg/passlock/multilocker_test.go b/pkg/passlock/multilocker_test.go index 87a0cb3..8a6bdd0 100644 --- a/pkg/passlock/multilocker_test.go +++ b/pkg/passlock/multilocker_test.go @@ -27,7 +27,7 @@ func TestMultikey_AddKey(t *testing.T) { mk = NewMultiLocker(gen) assert.NoError(t, mk.Read(&buf)) - assert.Len(t, mk.mkeys, 2) + assert.Len(t, mk.surKeys, 2) data, err := mk.Unlock("developer", []byte("s3cre+")) assert.NoError(t, err) assert.Equal(t, plaintext, string(data)) @@ -52,3 +52,28 @@ func TestMultiLocker_RemovePass(t *testing.T) { assert.NoError(t, mk.EnableUpdate([]byte(basePass))) assert.NoError(t, mk.RemovePass("other"), "Should be okay to update.") } + +func TestMultiLocker_ReLock(t *testing.T) { + var ( + plaintext = "A secret message" + newPlaintext = "Another secret message" + basePass = "passphrase" + ) + gen, err := NewKeyGenerator(SetShortDelayIterations()) + assert.NoError(t, err) + mk := NewMultiLocker(gen) + assert.NoError(t, mk.Lock([]byte(basePass), []byte(plaintext))) + + assert.NoError(t, mk.AddPass("developer", []byte("s3cre+"))) + assert.NoError(t, mk.AddPass("other", []byte("some other secret"))) + + mk.DisableUpdate() + assert.Error(t, mk.Lock([]byte(basePass), []byte(newPlaintext))) + + assert.NoError(t, mk.EnableUpdate([]byte(basePass))) + assert.NoError(t, mk.Lock([]byte(basePass), []byte(newPlaintext))) + + got, err := mk.Unlock("developer", []byte("s3cre+")) + assert.NoError(t, err) + assert.Equal(t, newPlaintext, string(got)) +} From 5f5b336721e139e9560974a33e27096a1dc4b013 Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Fri, 28 Jul 2023 01:59:13 -0500 Subject: [PATCH 11/17] Updating binmap dependency --- go.mod | 2 +- go.sum | 2 ++ pkg/passlock/multilocker.go | 23 +++++++++++++---------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 5444adb..9868e52 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/saylorsolutions/gocryptx go 1.19 require ( - github.com/saylorsolutions/binmap v0.2.0 + github.com/saylorsolutions/binmap v0.3.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.11.0 diff --git a/go.sum b/go.sum index 977b6c3..5718f5e 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/saylorsolutions/binmap v0.2.0 h1:HADBNp5OROKwpy/cdQxrLnAkoIFl4eD6g8ixzoi5vkc= github.com/saylorsolutions/binmap v0.2.0/go.mod h1:nNL5x213T4kD+n7Oe8j0cSDXym11HX9++T3immOI1hg= +github.com/saylorsolutions/binmap v0.3.0 h1:JtCzLOeZNjwBiK/ON9CIidkNDi8uvJAh+QSc11Uq6eg= +github.com/saylorsolutions/binmap v0.3.0/go.mod h1:nNL5x213T4kD+n7Oe8j0cSDXym11HX9++T3immOI1hg= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= diff --git a/pkg/passlock/multilocker.go b/pkg/passlock/multilocker.go index 384ebdb..c06a0ff 100644 --- a/pkg/passlock/multilocker.go +++ b/pkg/passlock/multilocker.go @@ -71,17 +71,20 @@ func (l *MultiLocker) mapper() bin.Mapper { return k.mapper() }), l.keyGen.mapper(), - bin.Any(&l.payload, func(r io.Reader, endian binary.ByteOrder) error { - payload, err := io.ReadAll(r) - if err != nil { + bin.Any( + func(r io.Reader, endian binary.ByteOrder) error { + payload, err := io.ReadAll(r) + if err != nil { + return err + } + l.payload = payload + return nil + }, + func(w io.Writer, endian binary.ByteOrder) error { + _, err := io.Copy(w, bytes.NewReader(l.payload)) return err - } - l.payload = payload - return nil - }, func(w io.Writer, endian binary.ByteOrder) error { - _, err := io.Copy(w, bytes.NewReader(l.payload)) - return err - }), + }, + ), ) } From a226f344c02a927f8f39f26727d4f91d9981b207 Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Sat, 29 Jul 2023 16:28:24 -0500 Subject: [PATCH 12/17] Using typed byte slices to help make the API easier to use --- pkg/passlock/key.go | 32 +++++++++++++++++++++++++++----- pkg/passlock/locker.go | 4 ++-- pkg/passlock/multilocker.go | 23 +++++++++++++---------- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/pkg/passlock/key.go b/pkg/passlock/key.go index 39e1c2b..c37644c 100644 --- a/pkg/passlock/key.go +++ b/pkg/passlock/key.go @@ -22,6 +22,21 @@ var ( ErrInvalidData = errors.New("unable to use input data") ) +// Key is an AES key that can be used to encrypt or decrypt an encrypted payload. +type Key []byte + +// Salt is a slice of secure random bytes that is used with scrypt to generate a Key from a Passphrase. +type Salt []byte + +// Passphrase is a human-readable string used to generate a Key. +type Passphrase []byte + +// Encrypted is an encrypted payload. +type Encrypted []byte + +// Plaintext is an unencrypted payload. +type Plaintext []byte + type KeyGenerator struct { iterations uint64 relativeBlockSize uint8 @@ -130,11 +145,11 @@ func NewKeyGenerator(opts ...GeneratorOpt) (*KeyGenerator, error) { } // GenerateKey will generate an AES key and salt using the configuration of the KeyGenerator. -func (g *KeyGenerator) GenerateKey(pass []byte) (key, salt []byte, err error) { +func (g *KeyGenerator) GenerateKey(pass Passphrase) (key Key, salt Salt, err error) { if len(pass) == 0 { return nil, nil, ErrEmptyPassPhrase } - salt = make([]byte, g.aesKeySize) + salt = make(Salt, g.aesKeySize) if _, err = rand.Read(salt); err != nil { return nil, nil, err } @@ -144,24 +159,31 @@ func (g *KeyGenerator) GenerateKey(pass []byte) (key, salt []byte, err error) { // DeriveKey will recover a key with the salt in the payload and the given passphrase. // This doesn't ensure that the given passphrase is the *correct* passphrase used to encrypt the payload. -func (g *KeyGenerator) DeriveKey(pass, data []byte) (key []byte, err error) { +func (g *KeyGenerator) DeriveKey(pass Passphrase, data Encrypted) (key Key, err error) { key, _, err = g.DeriveKeySalt(pass, data) return key, err } // DeriveKeySalt will recover a key and the original salt in the payload with the given passphrase. // This doesn't ensure that the given passphrase is the *correct* passphrase used to encrypt the payload. -func (g *KeyGenerator) DeriveKeySalt(pass, data []byte) (key []byte, salt []byte, err error) { +func (g *KeyGenerator) DeriveKeySalt(pass Passphrase, data Encrypted) (key Key, salt Salt, err error) { if len(pass) == 0 { return nil, nil, ErrEmptyPassPhrase } if len(data) <= int(g.aesKeySize) { return nil, nil, fmt.Errorf("%w: input data isn't long enough to contain a key salt", ErrInvalidData) } - salt = data[len(data)-int(g.aesKeySize):] + salt = Salt(data[len(data)-int(g.aesKeySize):]) key, err = scrypt.Key(pass, salt, int(g.iterations), int(g.relativeBlockSize), int(g.cpuCost), int(g.aesKeySize)) if err != nil { return nil, nil, err } return key, salt, nil } + +func (g *KeyGenerator) DeriveSalt(data Encrypted) (salt Salt, err error) { + if uint64(len(data)) <= uint64(g.aesKeySize) { + return nil, fmt.Errorf("%w: data is not long enough to contain a valid salt", ErrInvalidData) + } + return Salt(data[:len(data)-int(g.aesKeySize)]), nil +} diff --git a/pkg/passlock/locker.go b/pkg/passlock/locker.go index 1b8ca44..215e4c5 100644 --- a/pkg/passlock/locker.go +++ b/pkg/passlock/locker.go @@ -9,7 +9,7 @@ import ( // Lock will encrypt the payload with the given key, and append the given salt to the payload. // Exposure of the salt doesn't weaken the key, since the passphrase is also required to arrive at the same key. // However, tampering with the salt or the payload would prevent Unlock from recovering the plaintext payload. -func Lock(key, salt, data []byte) ([]byte, error) { +func Lock(key Key, salt Salt, data Plaintext) (Encrypted, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err @@ -30,7 +30,7 @@ func Lock(key, salt, data []byte) ([]byte, error) { // Unlock will decrypt the payload after stripping the salt from the end of it. // The salt length is expected to match the key length (which is enforced by KeyGenerator). -func Unlock(key, data []byte) ([]byte, error) { +func Unlock(key Key, data Encrypted) (Plaintext, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err diff --git a/pkg/passlock/multilocker.go b/pkg/passlock/multilocker.go index c06a0ff..96d38fe 100644 --- a/pkg/passlock/multilocker.go +++ b/pkg/passlock/multilocker.go @@ -21,21 +21,21 @@ var ( type surrogateKey struct { id string - encryptedKey []byte + encryptedKey Encrypted } func (k *surrogateKey) mapper() bin.Mapper { return bin.MapSequence( bin.FixedString(&k.id, idFieldLen), - bin.DynamicSlice(&k.encryptedKey, bin.Byte), + bin.DynamicSlice((*[]byte)(&k.encryptedKey), bin.Byte), ) } type MultiLocker struct { surKeys []surrogateKey - payload []byte + payload Encrypted - baseKey []byte + baseKey Key keyGen *KeyGenerator } @@ -131,7 +131,7 @@ func (l *MultiLocker) ListKeyIDs() []string { return ids } -func (l *MultiLocker) AddPass(id string, pass []byte) error { +func (l *MultiLocker) AddPass(id string, pass Passphrase) error { if len(id) > idFieldLen || len(id) == 0 { return fmt.Errorf("id value is not within the valid range of 1-%d bytes", idFieldLen) } @@ -142,7 +142,8 @@ func (l *MultiLocker) AddPass(id string, pass []byte) error { if err != nil { return err } - encryptedKey, err := Lock(newPassKey, salt, l.baseKey) + plainKey := Plaintext(l.baseKey) + encryptedKey, err := Lock(newPassKey, salt, plainKey) if err != nil { return err } @@ -167,7 +168,7 @@ func (l *MultiLocker) RemovePass(id string) error { return nil } -func (l *MultiLocker) UpdatePass(id string, pass []byte) error { +func (l *MultiLocker) UpdatePass(id string, pass Passphrase) error { if len(id) > idFieldLen { return fmt.Errorf("id value is greater than the maximum field width of %d", idFieldLen) } @@ -178,7 +179,8 @@ func (l *MultiLocker) UpdatePass(id string, pass []byte) error { if err != nil { return err } - encryptedKey, err := Lock(newPassKey, salt, l.baseKey) + plainKey := Plaintext(l.baseKey) + encryptedKey, err := Lock(newPassKey, salt, plainKey) if err != nil { return err } @@ -236,14 +238,15 @@ func (l *MultiLocker) Unlock(id string, pass []byte) ([]byte, error) { if err != nil { return nil, err } - baseKey, err := Unlock(passKey, mk.encryptedKey) + unencKey, err := Unlock(passKey, mk.encryptedKey) if err != nil { return nil, ErrInvalidPassword } + baseKey := Key(unencKey) data, err := Unlock(baseKey, l.payload) baseKey = nil if err != nil { - return nil, fmt.Errorf("%w: invalid base pass", ErrInvalidPassword) + return nil, fmt.Errorf("%w: invalid base key", ErrInvalidPassword) } return data, nil } From e1ef3e4835eef2d85846119968dbd62b2cb385ee Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Wed, 2 Aug 2023 23:33:34 -0500 Subject: [PATCH 13/17] Adding WriteMultiLocker - Allows overriding the logical constraint that surrogate keys may not update the locked payload. --- pkg/passlock/doc.go | 3 +- pkg/passlock/key.go | 4 +- pkg/passlock/multilocker.go | 69 ++++++++++++++++++++++++++------ pkg/passlock/multilocker_test.go | 48 +++++++++++++++++----- 4 files changed, 99 insertions(+), 25 deletions(-) diff --git a/pkg/passlock/doc.go b/pkg/passlock/doc.go index 546de62..4444ecf 100644 --- a/pkg/passlock/doc.go +++ b/pkg/passlock/doc.go @@ -18,7 +18,8 @@ A MultiLocker with surrogate keys and encrypted payload may be persisted to disk A freshly read MultiLocker may not be changed in any way. Editing is enabled by calling EnableUpdate with the base passphrase. After this call completes successfully, surrogate keys may be added or removed. -A new encrypted payload may not be set to a MultiLocker if surrogate keys exist, and the MultiLocker has not had EnableUpdate called. Allowing this operation would invalidate those keys otherwise. +A new encrypted payload may only be set in a MultiLocker with the base passphrase. +A WriteMultiLocker allows surrogate passphrases to be used to lock a new payload, instead of just the base passphrase. # General guidelines: - It's possible to customize the CPU cost, iteration count, and relative block size parameters directly for key generation. If you're not an expert, then don't use SetIterations, SetCPUCost, or SetRelativeBlockSize. diff --git a/pkg/passlock/key.go b/pkg/passlock/key.go index c37644c..5f50eae 100644 --- a/pkg/passlock/key.go +++ b/pkg/passlock/key.go @@ -170,7 +170,7 @@ func (g *KeyGenerator) DeriveKeySalt(pass Passphrase, data Encrypted) (key Key, if len(pass) == 0 { return nil, nil, ErrEmptyPassPhrase } - if len(data) <= int(g.aesKeySize) { + if uint64(len(data)) <= uint64(g.aesKeySize) { return nil, nil, fmt.Errorf("%w: input data isn't long enough to contain a key salt", ErrInvalidData) } salt = Salt(data[len(data)-int(g.aesKeySize):]) @@ -185,5 +185,5 @@ func (g *KeyGenerator) DeriveSalt(data Encrypted) (salt Salt, err error) { if uint64(len(data)) <= uint64(g.aesKeySize) { return nil, fmt.Errorf("%w: data is not long enough to contain a valid salt", ErrInvalidData) } - return Salt(data[:len(data)-int(g.aesKeySize)]), nil + return Salt(data[len(data)-int(g.aesKeySize):]), nil } diff --git a/pkg/passlock/multilocker.go b/pkg/passlock/multilocker.go index 96d38fe..21a5c98 100644 --- a/pkg/passlock/multilocker.go +++ b/pkg/passlock/multilocker.go @@ -47,7 +47,7 @@ func NewMultiLocker(gen *KeyGenerator) *MultiLocker { func (l *MultiLocker) validateInitialized() error { if len(l.payload) == 0 { - return errors.New("no payload set for update") + return errors.New("no payload set") } if l.keyGen == nil { return errors.New("no generator set") @@ -131,7 +131,7 @@ func (l *MultiLocker) ListKeyIDs() []string { return ids } -func (l *MultiLocker) AddPass(id string, pass Passphrase) error { +func (l *MultiLocker) AddSurrogatePass(id string, pass Passphrase) error { if len(id) > idFieldLen || len(id) == 0 { return fmt.Errorf("id value is not within the valid range of 1-%d bytes", idFieldLen) } @@ -155,7 +155,7 @@ func (l *MultiLocker) AddPass(id string, pass Passphrase) error { return nil } -func (l *MultiLocker) RemovePass(id string) error { +func (l *MultiLocker) RemoveSurrogatePass(id string) error { if err := l.validateForUpdate(); err != nil { return err } @@ -168,14 +168,14 @@ func (l *MultiLocker) RemovePass(id string) error { return nil } -func (l *MultiLocker) UpdatePass(id string, pass Passphrase) error { +func (l *MultiLocker) UpdateSurrogatePass(id string, newPass Passphrase) error { if len(id) > idFieldLen { return fmt.Errorf("id value is greater than the maximum field width of %d", idFieldLen) } if err := l.validateForUpdate(); err != nil { return err } - newPassKey, salt, err := l.keyGen.GenerateKey(pass) + newPassKey, salt, err := l.keyGen.GenerateKey(newPass) if err != nil { return err } @@ -198,17 +198,18 @@ func (l *MultiLocker) Lock(pass []byte, unencrypted []byte) error { return errors.New("missing key generator") } if len(l.surKeys) > 0 { - if err := l.validateForUpdate(); err != nil { + if err := l.validateInitialized(); err != nil { return fmt.Errorf("cannot Lock a new payload with surrogate keys until update is enabled: %w", err) } key, salt, err := l.keyGen.DeriveKeySalt(pass, l.payload) if err != nil { return err } - if !bytes.Equal(key, l.baseKey) { - return fmt.Errorf("%w: passphrase doesn't match base passphrase", ErrInvalidPassword) + _, err = Unlock(key, l.payload) + if err != nil { + return ErrInvalidPassword } - newPayload, err := Lock(l.baseKey, salt, unencrypted) + newPayload, err := Lock(key, salt, unencrypted) if err != nil { return err } @@ -232,13 +233,13 @@ func (l *MultiLocker) Unlock(id string, pass []byte) ([]byte, error) { if err := l.validateInitialized(); err != nil { return nil, err } - for _, mk := range l.surKeys { - if mk.id == id { - passKey, err := l.keyGen.DeriveKey(pass, mk.encryptedKey) + for _, sur := range l.surKeys { + if sur.id == id { + passKey, err := l.keyGen.DeriveKey(pass, sur.encryptedKey) if err != nil { return nil, err } - unencKey, err := Unlock(passKey, mk.encryptedKey) + unencKey, err := Unlock(passKey, sur.encryptedKey) if err != nil { return nil, ErrInvalidPassword } @@ -253,3 +254,45 @@ func (l *MultiLocker) Unlock(id string, pass []byte) ([]byte, error) { } return nil, errors.New("multikey ID not found") } + +// WriteMultiLocker is the same as MultiLocker, except that the logical constraint that surrogate keys cannot write a new payload is lifted. +type WriteMultiLocker struct { + *MultiLocker +} + +func NewWriteMultiLocker(gen *KeyGenerator) *WriteMultiLocker { + return &WriteMultiLocker{ + MultiLocker: NewMultiLocker(gen), + } +} + +func (l *WriteMultiLocker) SurrogateLock(id string, pass Passphrase, unencrypted Plaintext) error { + if err := l.validateInitialized(); err != nil { + return err + } + for _, sur := range l.surKeys { + if sur.id == id { + passKey, err := l.keyGen.DeriveKey(pass, sur.encryptedKey) + if err != nil { + return err + } + unencKey, err := Unlock(passKey, sur.encryptedKey) + if err != nil { + return ErrInvalidPassword + } + baseKey := Key(unencKey) + _, err = Unlock(baseKey, l.payload) + if err != nil { + return fmt.Errorf("%w: invalid base key", ErrInvalidPassword) + } + salt, err := l.keyGen.DeriveSalt(l.payload) + if err != nil { + return err + } + l.payload, err = Lock(baseKey, salt, unencrypted) + baseKey = nil + return err + } + } + return errors.New("multikey ID not found") +} diff --git a/pkg/passlock/multilocker_test.go b/pkg/passlock/multilocker_test.go index 8a6bdd0..6f73d51 100644 --- a/pkg/passlock/multilocker_test.go +++ b/pkg/passlock/multilocker_test.go @@ -17,8 +17,8 @@ func TestMultikey_AddKey(t *testing.T) { mk := NewMultiLocker(gen) assert.NoError(t, mk.Lock([]byte(basePass), []byte(plaintext))) - assert.NoError(t, mk.AddPass("developer", []byte("s3cre+"))) - assert.NoError(t, mk.AddPass("other", []byte("some other secret"))) + assert.NoError(t, mk.AddSurrogatePass("developer", []byte("s3cre+"))) + assert.NoError(t, mk.AddSurrogatePass("other", []byte("some other secret"))) assert.NoError(t, mk.Write(&buf)) mkdata := buf.Bytes() @@ -43,14 +43,14 @@ func TestMultiLocker_RemovePass(t *testing.T) { mk := NewMultiLocker(gen) assert.NoError(t, mk.Lock([]byte(basePass), []byte(plaintext))) - assert.NoError(t, mk.AddPass("developer", []byte("s3cre+"))) - assert.NoError(t, mk.AddPass("other", []byte("some other secret"))) + assert.NoError(t, mk.AddSurrogatePass("developer", []byte("s3cre+"))) + assert.NoError(t, mk.AddSurrogatePass("other", []byte("some other secret"))) mk.DisableUpdate() - assert.Error(t, mk.RemovePass("other"), "Should return an error when update is not enabled.") + assert.Error(t, mk.RemoveSurrogatePass("other"), "Should return an error when update is not enabled.") assert.NoError(t, mk.EnableUpdate([]byte(basePass))) - assert.NoError(t, mk.RemovePass("other"), "Should be okay to update.") + assert.NoError(t, mk.RemoveSurrogatePass("other"), "Should be okay to update.") } func TestMultiLocker_ReLock(t *testing.T) { @@ -64,11 +64,36 @@ func TestMultiLocker_ReLock(t *testing.T) { mk := NewMultiLocker(gen) assert.NoError(t, mk.Lock([]byte(basePass), []byte(plaintext))) - assert.NoError(t, mk.AddPass("developer", []byte("s3cre+"))) - assert.NoError(t, mk.AddPass("other", []byte("some other secret"))) + assert.NoError(t, mk.AddSurrogatePass("developer", []byte("s3cre+"))) + assert.NoError(t, mk.AddSurrogatePass("other", []byte("some other secret"))) mk.DisableUpdate() - assert.Error(t, mk.Lock([]byte(basePass), []byte(newPlaintext))) + assert.NoError(t, mk.Lock([]byte(basePass), []byte(newPlaintext))) + + assert.NoError(t, mk.EnableUpdate([]byte(basePass))) + assert.NoError(t, mk.Lock([]byte(basePass), []byte(newPlaintext))) + + got, err := mk.Unlock("developer", []byte("s3cre+")) + assert.NoError(t, err) + assert.Equal(t, newPlaintext, string(got)) +} + +func TestWriteMultiLocker_SurrogateLock(t *testing.T) { + var ( + plaintext = "A secret message" + newPlaintext = "Another secret message" + basePass = "passphrase" + ) + gen, err := NewKeyGenerator(SetShortDelayIterations()) + assert.NoError(t, err) + mk := NewWriteMultiLocker(gen) + assert.NoError(t, mk.Lock([]byte(basePass), []byte(plaintext))) + + assert.NoError(t, mk.AddSurrogatePass("developer", []byte("s3cre+"))) + assert.NoError(t, mk.AddSurrogatePass("other", []byte("some other secret"))) + + mk.DisableUpdate() + assert.NoError(t, mk.Lock([]byte(basePass), []byte(newPlaintext))) assert.NoError(t, mk.EnableUpdate([]byte(basePass))) assert.NoError(t, mk.Lock([]byte(basePass), []byte(newPlaintext))) @@ -76,4 +101,9 @@ func TestMultiLocker_ReLock(t *testing.T) { got, err := mk.Unlock("developer", []byte("s3cre+")) assert.NoError(t, err) assert.Equal(t, newPlaintext, string(got)) + + assert.NoError(t, mk.SurrogateLock("other", []byte("some other secret"), []byte(plaintext))) + got, err = mk.Unlock("developer", []byte("s3cre+")) + assert.NoError(t, err) + assert.Equal(t, plaintext, string(got)) } From fea903052cf8aed6ed8c2f3798d73b0c70d240d4 Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Wed, 2 Aug 2023 23:34:54 -0500 Subject: [PATCH 14/17] Updating to newest binmap --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 9868e52..4089f24 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/saylorsolutions/gocryptx go 1.19 require ( - github.com/saylorsolutions/binmap v0.3.0 + github.com/saylorsolutions/binmap v0.4.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.11.0 diff --git a/go.sum b/go.sum index 5718f5e..587790f 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/saylorsolutions/binmap v0.2.0 h1:HADBNp5OROKwpy/cdQxrLnAkoIFl4eD6g8ix github.com/saylorsolutions/binmap v0.2.0/go.mod h1:nNL5x213T4kD+n7Oe8j0cSDXym11HX9++T3immOI1hg= github.com/saylorsolutions/binmap v0.3.0 h1:JtCzLOeZNjwBiK/ON9CIidkNDi8uvJAh+QSc11Uq6eg= github.com/saylorsolutions/binmap v0.3.0/go.mod h1:nNL5x213T4kD+n7Oe8j0cSDXym11HX9++T3immOI1hg= +github.com/saylorsolutions/binmap v0.4.0 h1:khmenzTziUUPi7ZZaaBbEJnJXTFnKNBODjLW+L9gz98= +github.com/saylorsolutions/binmap v0.4.0/go.mod h1:nNL5x213T4kD+n7Oe8j0cSDXym11HX9++T3immOI1hg= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= From f290510888e4925c163d952238aef12cc40a1271 Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Wed, 2 Aug 2023 23:58:05 -0500 Subject: [PATCH 15/17] Switching to map for easier surrogate key lookups --- pkg/passlock/multilocker.go | 137 +++++++++++++++---------------- pkg/passlock/multilocker_test.go | 8 +- 2 files changed, 68 insertions(+), 77 deletions(-) diff --git a/pkg/passlock/multilocker.go b/pkg/passlock/multilocker.go index 21a5c98..9b102a1 100644 --- a/pkg/passlock/multilocker.go +++ b/pkg/passlock/multilocker.go @@ -20,19 +20,11 @@ var ( ) type surrogateKey struct { - id string encryptedKey Encrypted } -func (k *surrogateKey) mapper() bin.Mapper { - return bin.MapSequence( - bin.FixedString(&k.id, idFieldLen), - bin.DynamicSlice((*[]byte)(&k.encryptedKey), bin.Byte), - ) -} - type MultiLocker struct { - surKeys []surrogateKey + surKeys map[string]surrogateKey payload Encrypted baseKey Key @@ -41,7 +33,8 @@ type MultiLocker struct { func NewMultiLocker(gen *KeyGenerator) *MultiLocker { return &MultiLocker{ - keyGen: gen, + keyGen: gen, + surKeys: map[string]surrogateKey{}, } } @@ -67,8 +60,12 @@ func (l *MultiLocker) validateForUpdate() error { func (l *MultiLocker) mapper() bin.Mapper { return bin.MapSequence( - bin.DynamicSlice(&l.surKeys, func(k *surrogateKey) bin.Mapper { - return k.mapper() + bin.Map(&l.surKeys, func(key *string) bin.Mapper { + return bin.FixedString(key, idFieldLen) + }, func(val *surrogateKey) bin.Mapper { + return bin.DynamicSlice((*[]byte)(&val.encryptedKey), func(e *byte) bin.Mapper { + return bin.Byte(e) + }) }), l.keyGen.mapper(), bin.Any( @@ -124,8 +121,8 @@ func (l *MultiLocker) DisableUpdate() { func (l *MultiLocker) ListKeyIDs() []string { ids := make([]string, len(l.surKeys)) - for i, mk := range l.surKeys { - ids[i] = mk.id + for id := range l.surKeys { + ids = append(ids, id) } sort.Strings(ids) return ids @@ -135,6 +132,9 @@ func (l *MultiLocker) AddSurrogatePass(id string, pass Passphrase) error { if len(id) > idFieldLen || len(id) == 0 { return fmt.Errorf("id value is not within the valid range of 1-%d bytes", idFieldLen) } + if _, ok := l.surKeys[id]; ok { + return fmt.Errorf("surrogate key ID already exists") + } if err := l.validateForUpdate(); err != nil { return err } @@ -148,10 +148,9 @@ func (l *MultiLocker) AddSurrogatePass(id string, pass Passphrase) error { return err } newKey := surrogateKey{ - id: id, encryptedKey: encryptedKey, } - l.surKeys = append(l.surKeys, newKey) + l.surKeys[id] = newKey return nil } @@ -159,12 +158,11 @@ func (l *MultiLocker) RemoveSurrogatePass(id string) error { if err := l.validateForUpdate(); err != nil { return err } - for i := 0; i < len(l.surKeys); i++ { - if l.surKeys[i].id == id { - l.surKeys = append(l.surKeys[:i], l.surKeys[i+1:]...) - return nil - } + _, ok := l.surKeys[id] + if !ok { + return nil } + delete(l.surKeys, id) return nil } @@ -172,6 +170,10 @@ func (l *MultiLocker) UpdateSurrogatePass(id string, newPass Passphrase) error { if len(id) > idFieldLen { return fmt.Errorf("id value is greater than the maximum field width of %d", idFieldLen) } + sur, ok := l.surKeys[id] + if !ok { + return errors.New("given ID is not present in this MultiLocker") + } if err := l.validateForUpdate(); err != nil { return err } @@ -184,13 +186,8 @@ func (l *MultiLocker) UpdateSurrogatePass(id string, newPass Passphrase) error { if err != nil { return err } - for _, mk := range l.surKeys { - if mk.id == id { - mk.encryptedKey = encryptedKey - return nil - } - } - return errors.New("given ID is not present in this MultiLocker") + sur.encryptedKey = encryptedKey + return nil } func (l *MultiLocker) Lock(pass []byte, unencrypted []byte) error { @@ -233,26 +230,25 @@ func (l *MultiLocker) Unlock(id string, pass []byte) ([]byte, error) { if err := l.validateInitialized(); err != nil { return nil, err } - for _, sur := range l.surKeys { - if sur.id == id { - passKey, err := l.keyGen.DeriveKey(pass, sur.encryptedKey) - if err != nil { - return nil, err - } - unencKey, err := Unlock(passKey, sur.encryptedKey) - if err != nil { - return nil, ErrInvalidPassword - } - baseKey := Key(unencKey) - data, err := Unlock(baseKey, l.payload) - baseKey = nil - if err != nil { - return nil, fmt.Errorf("%w: invalid base key", ErrInvalidPassword) - } - return data, nil - } + sur, ok := l.surKeys[id] + if !ok { + return nil, errors.New("surrogate key ID not found") + } + passKey, err := l.keyGen.DeriveKey(pass, sur.encryptedKey) + if err != nil { + return nil, err + } + unencKey, err := Unlock(passKey, sur.encryptedKey) + if err != nil { + return nil, ErrInvalidPassword } - return nil, errors.New("multikey ID not found") + baseKey := Key(unencKey) + data, err := Unlock(baseKey, l.payload) + baseKey = nil + if err != nil { + return nil, fmt.Errorf("%w: invalid base key", ErrInvalidPassword) + } + return data, nil } // WriteMultiLocker is the same as MultiLocker, except that the logical constraint that surrogate keys cannot write a new payload is lifted. @@ -270,29 +266,28 @@ func (l *WriteMultiLocker) SurrogateLock(id string, pass Passphrase, unencrypted if err := l.validateInitialized(); err != nil { return err } - for _, sur := range l.surKeys { - if sur.id == id { - passKey, err := l.keyGen.DeriveKey(pass, sur.encryptedKey) - if err != nil { - return err - } - unencKey, err := Unlock(passKey, sur.encryptedKey) - if err != nil { - return ErrInvalidPassword - } - baseKey := Key(unencKey) - _, err = Unlock(baseKey, l.payload) - if err != nil { - return fmt.Errorf("%w: invalid base key", ErrInvalidPassword) - } - salt, err := l.keyGen.DeriveSalt(l.payload) - if err != nil { - return err - } - l.payload, err = Lock(baseKey, salt, unencrypted) - baseKey = nil - return err - } + sur, ok := l.surKeys[id] + if !ok { + return errors.New("surrogate key ID not found") + } + passKey, err := l.keyGen.DeriveKey(pass, sur.encryptedKey) + if err != nil { + return err + } + unencKey, err := Unlock(passKey, sur.encryptedKey) + if err != nil { + return ErrInvalidPassword + } + baseKey := Key(unencKey) + _, err = Unlock(baseKey, l.payload) + if err != nil { + return fmt.Errorf("%w: invalid base key", ErrInvalidPassword) + } + salt, err := l.keyGen.DeriveSalt(l.payload) + if err != nil { + return err } - return errors.New("multikey ID not found") + l.payload, err = Lock(baseKey, salt, unencrypted) + baseKey = nil + return err } diff --git a/pkg/passlock/multilocker_test.go b/pkg/passlock/multilocker_test.go index 6f73d51..2c2c8ce 100644 --- a/pkg/passlock/multilocker_test.go +++ b/pkg/passlock/multilocker_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -func TestMultikey_AddKey(t *testing.T) { +func TestMultiLocker_AddSurrogatePass(t *testing.T) { var ( plaintext = "A secret message" basePass = "passphrase" @@ -21,10 +21,6 @@ func TestMultikey_AddKey(t *testing.T) { assert.NoError(t, mk.AddSurrogatePass("other", []byte("some other secret"))) assert.NoError(t, mk.Write(&buf)) - mkdata := buf.Bytes() - buf.Reset() - buf.Write(mkdata) - mk = NewMultiLocker(gen) assert.NoError(t, mk.Read(&buf)) assert.Len(t, mk.surKeys, 2) @@ -33,7 +29,7 @@ func TestMultikey_AddKey(t *testing.T) { assert.Equal(t, plaintext, string(data)) } -func TestMultiLocker_RemovePass(t *testing.T) { +func TestMultiLocker_RemoveSurrogatePass(t *testing.T) { var ( plaintext = "A secret message" basePass = "passphrase" From be93bb7aa0ca14716ed6502ae5e7e935e9dc462b Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Sun, 6 Aug 2023 13:56:40 -0500 Subject: [PATCH 16/17] Adding more explanation to the docs --- pkg/passlock/doc.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/pkg/passlock/doc.go b/pkg/passlock/doc.go index 4444ecf..dc615b7 100644 --- a/pkg/passlock/doc.go +++ b/pkg/passlock/doc.go @@ -2,10 +2,23 @@ Package passlock provides functions for encrypting data using a key derived from a user-provided passphrase. This uses AES-256 encryption to encrypt the provided data. +# Definition of terms: + + - plaintext: Data that has not been encrypted. + - passphrase: A user-defined phrase used in place of an encryption key. Longer is better. + - key: A key is a byte array of a size specific to the encryption algorithm used. An AES-128 key may not be used for AES-256 encryption operations. + - salt: A salt is a generated byte array that is the same size as the key. This is generated using a secure random source, and is the entropy used for key generation and recovery. + - key derivation: The process of securely creating a key from secure inputs. This process should be practically infeasible to reproduce without knowing the original inputs. + - scrypt: A CPU and memory hard algorithm used for key derivation in this library. This was designed as an improvement to bcrypt to combat GPU brute force attacks. + - AES: Stands for Advanced Encryption Standard. This is an encryption algorithm that - at the time of this writing - has not been shown to be directly broken in any way. + - base key: The key used to encrypt/decrypt the payload in a MultiLocker. + - surrogate key: A key used to encrypt the base key in a MultiLocker. This allows different passphrases to be used with the same encrypted payload. + # How it works: A key and salt is generated from the given passphrase. The salt is appended to the encrypted payload so the same key can be derived later given the same passphrase. Scrypt is memory and CPU hard, so it's impractical to brute force the salt to get the original passphrase, provided that sufficient tuning values are provided to the KeyGenerator. +A normal user is really only expected to define an iteration count for key generation. The key, salt, and plaintext are passed to the Lock function to encrypt the payload and append the salt to it. The key is recovered from the encrypted payload by passing the original passphrase and the payload to KeyGenerator.Derive. @@ -24,11 +37,13 @@ A WriteMultiLocker allows surrogate passphrases to be used to lock a new payload # General guidelines: - It's possible to customize the CPU cost, iteration count, and relative block size parameters directly for key generation. If you're not an expert, then don't use SetIterations, SetCPUCost, or SetRelativeBlockSize. - Both short and long delay iteration GeneratorOpt functions are provided, choose the correct iterations for your use-case using either SetLongDelayIterations or SetShortDelayIterations. + - If encrypted data is intended to be stored indefinitely, choose the SetLongDelayIterations option for key generation. - This method of encryption (AES-GCM) supports encrypting and authenticating at most about 64GB at a time. You could get around this by splitting a very large file into multiple chunks that include some metadata to prevent reordering or truncating. - AES-256 is a good default for a lot of cases, with excellent security and good throughput speeds. - This library supports AES-256 since that is the best supported by the Go standard lib, but AES-128 may also be used for situations where more throughput is desired. - - The main limit to throughput comes from key generation, the AES key size makes a much smaller impact to performance. - - When deriving the key from an encrypted payload, make sure that the same KeyGenerator settings are used. Not doing so will likely result in an incorrect key. + - The main limit to operation speed comes from key generation (as intended), the AES key size makes a much smaller impact to performance unless a very large payload is encrypted. + - When deriving the key from an encrypted payload, make sure that the same KeyGenerator settings are used. Not doing so will result in an incorrect key. - Technically, a surrogate key could be used to update a MultiLocker encrypted payload without invalidating other surrogate keys, since there are no cryptographic blockers to that. The base MultiLocker doesn't provide that function as a logical constraint only. + - The MultiLocker base key may not be updated without invalidating all surrogate keys. */ package passlock From bf9c07e320ba09bb0ad14f3acf49b15f41c34acf Mon Sep 17 00:00:00 2001 From: Doug Saylor Date: Sun, 6 Aug 2023 15:58:18 -0500 Subject: [PATCH 17/17] Adding additional documentation --- pkg/passlock/doc.go | 12 ++++++++---- pkg/passlock/key.go | 4 ++++ pkg/passlock/locker.go | 11 ++++++----- pkg/passlock/multilocker.go | 34 ++++++++++++++++++++++++++++------ 4 files changed, 46 insertions(+), 15 deletions(-) diff --git a/pkg/passlock/doc.go b/pkg/passlock/doc.go index dc615b7..7e2f9e9 100644 --- a/pkg/passlock/doc.go +++ b/pkg/passlock/doc.go @@ -9,8 +9,8 @@ This uses AES-256 encryption to encrypt the provided data. - key: A key is a byte array of a size specific to the encryption algorithm used. An AES-128 key may not be used for AES-256 encryption operations. - salt: A salt is a generated byte array that is the same size as the key. This is generated using a secure random source, and is the entropy used for key generation and recovery. - key derivation: The process of securely creating a key from secure inputs. This process should be practically infeasible to reproduce without knowing the original inputs. - - scrypt: A CPU and memory hard algorithm used for key derivation in this library. This was designed as an improvement to bcrypt to combat GPU brute force attacks. - - AES: Stands for Advanced Encryption Standard. This is an encryption algorithm that - at the time of this writing - has not been shown to be directly broken in any way. + - [scrypt]: A CPU and memory hard algorithm used for key derivation in this library. This was designed as an improvement to bcrypt to combat GPU brute force attacks. + - [AES]: Stands for Advanced Encryption Standard. This is an encryption algorithm that - at the time of this writing - has not been shown to be directly broken in any way. Side-channel attacks are possible, but are a weakness of implementation and hardware. - base key: The key used to encrypt/decrypt the payload in a MultiLocker. - surrogate key: A key used to encrypt the base key in a MultiLocker. This allows different passphrases to be used with the same encrypted payload. @@ -25,7 +25,7 @@ The key is recovered from the encrypted payload by passing the original passphra The key and encrypted payload are passed to the Unlock function to decrypt the payload and return the original plain text. The MultiLocker type extends the functionality above by providing the ability to use multiple surrogate keys to interact with the encrypted payload. -A MultiLocker is created using a KeyGenerator, and the encrypted payload is set by calling MultiLocker.Lock with the base passphrase and plaintext. +A MultiLocker is created using a KeyGenerator, and the encrypted payload is set by calling [MultiLocker.Lock] with the base passphrase and plaintext. Once created, surrogate keys may be added to the MultiLocker that allow reading the encrypted payload. A MultiLocker with surrogate keys and encrypted payload may be persisted to disk in binary form, and read back - including key generation settings. @@ -38,12 +38,16 @@ A WriteMultiLocker allows surrogate passphrases to be used to lock a new payload - It's possible to customize the CPU cost, iteration count, and relative block size parameters directly for key generation. If you're not an expert, then don't use SetIterations, SetCPUCost, or SetRelativeBlockSize. - Both short and long delay iteration GeneratorOpt functions are provided, choose the correct iterations for your use-case using either SetLongDelayIterations or SetShortDelayIterations. - If encrypted data is intended to be stored indefinitely, choose the SetLongDelayIterations option for key generation. - - This method of encryption (AES-GCM) supports encrypting and authenticating at most about 64GB at a time. You could get around this by splitting a very large file into multiple chunks that include some metadata to prevent reordering or truncating. + - This method of encryption ([AES-GCM]) supports encrypting and authenticating at most about 64GB at a time. You could get around this by splitting a very large file into multiple chunks that include some metadata to prevent reordering or truncating. - AES-256 is a good default for a lot of cases, with excellent security and good throughput speeds. - This library supports AES-256 since that is the best supported by the Go standard lib, but AES-128 may also be used for situations where more throughput is desired. - The main limit to operation speed comes from key generation (as intended), the AES key size makes a much smaller impact to performance unless a very large payload is encrypted. - When deriving the key from an encrypted payload, make sure that the same KeyGenerator settings are used. Not doing so will result in an incorrect key. - Technically, a surrogate key could be used to update a MultiLocker encrypted payload without invalidating other surrogate keys, since there are no cryptographic blockers to that. The base MultiLocker doesn't provide that function as a logical constraint only. - The MultiLocker base key may not be updated without invalidating all surrogate keys. + +[scrypt]: https://en.wikipedia.org/wiki/Scrypt +[AES]: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard +[AES-GCM]: https://en.wikipedia.org/wiki/Galois/Counter_Mode */ package passlock diff --git a/pkg/passlock/key.go b/pkg/passlock/key.go index 5f50eae..6e59c7a 100644 --- a/pkg/passlock/key.go +++ b/pkg/passlock/key.go @@ -53,8 +53,10 @@ func (g *KeyGenerator) mapper() bin.Mapper { ) } +// GeneratorOpt is a function option to be used with NewKeyGenerator. type GeneratorOpt = func(*KeyGenerator) error +// SetAES256KeySize uses 256 bits (32 bytes) as the key size to be generated. func SetAES256KeySize() GeneratorOpt { return func(gen *KeyGenerator) error { gen.aesKeySize = AES256KeySize @@ -62,6 +64,7 @@ func SetAES256KeySize() GeneratorOpt { } } +// SetAES128KeySize uses 128 bits (16 bytes) as the key size to be generated. func SetAES128KeySize() GeneratorOpt { return func(gen *KeyGenerator) error { gen.aesKeySize = AES128KeySize @@ -181,6 +184,7 @@ func (g *KeyGenerator) DeriveKeySalt(pass Passphrase, data Encrypted) (key Key, return key, salt, nil } +// DeriveSalt gets the salt value from the encrypted payload. func (g *KeyGenerator) DeriveSalt(data Encrypted) (salt Salt, err error) { if uint64(len(data)) <= uint64(g.aesKeySize) { return nil, fmt.Errorf("%w: data is not long enough to contain a valid salt", ErrInvalidData) diff --git a/pkg/passlock/locker.go b/pkg/passlock/locker.go index 215e4c5..a8f8113 100644 --- a/pkg/passlock/locker.go +++ b/pkg/passlock/locker.go @@ -6,9 +6,10 @@ import ( "crypto/rand" ) -// Lock will encrypt the payload with the given key, and append the given salt to the payload. -// Exposure of the salt doesn't weaken the key, since the passphrase is also required to arrive at the same key. -// However, tampering with the salt or the payload would prevent Unlock from recovering the plaintext payload. +// Lock will encrypt the payload with the given Key, and append the given Salt to the payload. +// Exposure of the Salt doesn't weaken the Key, since the passphrase is also required to arrive at the same Key. +// Salt exposure is required to be able to derive the same Key from the same passphrase. +// However, tampering with the Salt or the payload would prevent Unlock from recovering the Plaintext payload. func Lock(key Key, salt Salt, data Plaintext) (Encrypted, error) { block, err := aes.NewCipher(key) if err != nil { @@ -28,8 +29,8 @@ func Lock(key Key, salt Salt, data Plaintext) (Encrypted, error) { return cipherText, nil } -// Unlock will decrypt the payload after stripping the salt from the end of it. -// The salt length is expected to match the key length (which is enforced by KeyGenerator). +// Unlock will decrypt the payload after stripping the Salt from the end of it. +// The Salt length is expected to match the Key length (which is enforced by KeyGenerator). func Unlock(key Key, data Encrypted) (Plaintext, error) { block, err := aes.NewCipher(key) if err != nil { diff --git a/pkg/passlock/multilocker.go b/pkg/passlock/multilocker.go index 9b102a1..288f65c 100644 --- a/pkg/passlock/multilocker.go +++ b/pkg/passlock/multilocker.go @@ -23,6 +23,8 @@ type surrogateKey struct { encryptedKey Encrypted } +// MultiLocker allows using surrogate keys - in addition to a base key - for reading an encrypted payload. +// If surrogate key writes are desired, then use the WriteMultiLocker instead. type MultiLocker struct { surKeys map[string]surrogateKey payload Encrypted @@ -85,6 +87,7 @@ func (l *MultiLocker) mapper() bin.Mapper { ) } +// Read will read the MultiLocker as a binary payload from the io.Reader. func (l *MultiLocker) Read(r io.Reader) error { if err := l.mapper().Read(r, binary.BigEndian); err != nil { return fmt.Errorf("%w: %v", ErrInvalidHeader, err) @@ -92,12 +95,13 @@ func (l *MultiLocker) Read(r io.Reader) error { return nil } +// Write will write the MultiLocker as a binary payload to the io.Writer. func (l *MultiLocker) Write(w io.Writer) error { return l.mapper().Write(w, binary.BigEndian) } // EnableUpdate validates the MultiLocker and ensures that it's in a suitable state for updating by setting the base key. -// The original payload passphrase must be used, not a surrogate passphrase, to validate that the correct key is populated. +// The original base passphrase must be used, not a surrogate passphrase, to validate that the correct key is populated. func (l *MultiLocker) EnableUpdate(pass []byte) error { err := l.validateInitialized() if err != nil { @@ -115,10 +119,12 @@ func (l *MultiLocker) EnableUpdate(pass []byte) error { return nil } +// DisableUpdate will disable updates to this MultiLocker. func (l *MultiLocker) DisableUpdate() { l.baseKey = nil } +// ListKeyIDs lists all surrogate key IDs in this MultiLocker. func (l *MultiLocker) ListKeyIDs() []string { ids := make([]string, len(l.surKeys)) for id := range l.surKeys { @@ -128,6 +134,8 @@ func (l *MultiLocker) ListKeyIDs() []string { return ids } +// AddSurrogatePass will add a new surrogate key to this MultiLocker. +// Update must be enabled in this MultiLocker before this can be done. func (l *MultiLocker) AddSurrogatePass(id string, pass Passphrase) error { if len(id) > idFieldLen || len(id) == 0 { return fmt.Errorf("id value is not within the valid range of 1-%d bytes", idFieldLen) @@ -154,6 +162,8 @@ func (l *MultiLocker) AddSurrogatePass(id string, pass Passphrase) error { return nil } +// RemoveSurrogatePass will remove a surrogate key. +// Update must be enabled in this MultiLocker before this can be done. func (l *MultiLocker) RemoveSurrogatePass(id string) error { if err := l.validateForUpdate(); err != nil { return err @@ -166,6 +176,8 @@ func (l *MultiLocker) RemoveSurrogatePass(id string) error { return nil } +// UpdateSurrogatePass will update the passphrase of an existing surrogate key by ID. +// Update must be enabled in this MultiLocker before this can be done. func (l *MultiLocker) UpdateSurrogatePass(id string, newPass Passphrase) error { if len(id) > idFieldLen { return fmt.Errorf("id value is greater than the maximum field width of %d", idFieldLen) @@ -190,22 +202,24 @@ func (l *MultiLocker) UpdateSurrogatePass(id string, newPass Passphrase) error { return nil } +// Lock will lock a new payload with the base key. +// If surrogate keys are present, then the same salt will be used to ensure that surrogate keys are not invalidated. func (l *MultiLocker) Lock(pass []byte, unencrypted []byte) error { - if l.keyGen == nil { - return errors.New("missing key generator") + if err := l.validateInitialized(); err != nil { + return err } if len(l.surKeys) > 0 { - if err := l.validateInitialized(); err != nil { - return fmt.Errorf("cannot Lock a new payload with surrogate keys until update is enabled: %w", err) - } + // Must maintain the same salt to avoid invalidating surrogate keys key, salt, err := l.keyGen.DeriveKeySalt(pass, l.payload) if err != nil { return err } + // Check that the key is valid _, err = Unlock(key, l.payload) if err != nil { return ErrInvalidPassword } + // Lock the new payload with the existing base key and the same salt newPayload, err := Lock(key, salt, unencrypted) if err != nil { return err @@ -217,6 +231,11 @@ func (l *MultiLocker) Lock(pass []byte, unencrypted []byte) error { if err != nil { return err } + // Check that the key is valid + _, err = Unlock(key, l.payload) + if err != nil { + return ErrInvalidPassword + } encrypted, err := Lock(key, salt, unencrypted) if err != nil { return err @@ -226,6 +245,7 @@ func (l *MultiLocker) Lock(pass []byte, unencrypted []byte) error { return nil } +// Unlock will unlock the payload with a surrogate key. func (l *MultiLocker) Unlock(id string, pass []byte) ([]byte, error) { if err := l.validateInitialized(); err != nil { return nil, err @@ -262,6 +282,7 @@ func NewWriteMultiLocker(gen *KeyGenerator) *WriteMultiLocker { } } +// SurrogateLock will Lock a new payload in the MultiLocker using a surrogate key. func (l *WriteMultiLocker) SurrogateLock(id string, pass Passphrase, unencrypted Plaintext) error { if err := l.validateInitialized(); err != nil { return err @@ -279,6 +300,7 @@ func (l *WriteMultiLocker) SurrogateLock(id string, pass Passphrase, unencrypted return ErrInvalidPassword } baseKey := Key(unencKey) + // Ensure the baseKey is valid _, err = Unlock(baseKey, l.payload) if err != nil { return fmt.Errorf("%w: invalid base key", ErrInvalidPassword)