-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from saylorsolutions/feature/passlock
Added password-based encryption, and the structure for using multiple "surrogate" keys to interact with the same encrypted payload.
- Loading branch information
Showing
10 changed files
with
859 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
/* | ||
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. 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. | ||
# 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. | ||
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 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. | ||
- 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 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
package passlock | ||
|
||
import ( | ||
"crypto/rand" | ||
"errors" | ||
"fmt" | ||
bin "github.com/saylorsolutions/binmap" | ||
"golang.org/x/crypto/scrypt" | ||
) | ||
|
||
const ( | ||
DefaultLargeIterations uint64 = 1 << 30 | ||
DefaultInteractiveIterations uint64 = 1 << 17 | ||
DefaultRelBlockSize uint8 = 8 | ||
DefaultCpuCost uint8 = 1 | ||
AES256KeySize uint8 = 256 / 8 | ||
AES128KeySize uint8 = 128 / 8 | ||
) | ||
|
||
var ( | ||
ErrEmptyPassPhrase = errors.New("cannot use an empty passphrase") | ||
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 | ||
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), | ||
) | ||
} | ||
|
||
// 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 | ||
return nil | ||
} | ||
} | ||
|
||
// SetAES128KeySize uses 128 bits (16 bytes) as the key size to be generated. | ||
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 { | ||
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 uint64) 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 uint8) 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 uint8) 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 Passphrase) (key Key, salt Salt, err error) { | ||
if len(pass) == 0 { | ||
return nil, nil, ErrEmptyPassPhrase | ||
} | ||
salt = make(Salt, g.aesKeySize) | ||
if _, err = rand.Read(salt); err != nil { | ||
return nil, nil, err | ||
} | ||
key, err = scrypt.Key(pass, salt, int(g.iterations), int(g.relativeBlockSize), int(g.cpuCost), int(g.aesKeySize)) | ||
return key, salt, err | ||
} | ||
|
||
// 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 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 Passphrase, data Encrypted) (key Key, salt Salt, err error) { | ||
if len(pass) == 0 { | ||
return nil, nil, ErrEmptyPassPhrase | ||
} | ||
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):]) | ||
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 | ||
} | ||
|
||
// 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) | ||
} | ||
return Salt(data[len(data)-int(g.aesKeySize):]), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package passlock | ||
|
||
import ( | ||
"bytes" | ||
"encoding/binary" | ||
"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, int(gen.aesKeySize)) | ||
assert.Len(t, salt, int(gen.aesKeySize)) | ||
} | ||
|
||
func TestNewKeyGenerator_Custom(t *testing.T) { | ||
gen, err := NewKeyGenerator( | ||
SetIterations(2), | ||
SetLongDelayIterations(), | ||
SetShortDelayIterations(), | ||
SetCPUCost(DefaultCpuCost), | ||
SetRelativeBlockSize(DefaultRelBlockSize), | ||
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, 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), | ||
SetAES128KeySize(), | ||
) | ||
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) | ||
} |
Oops, something went wrong.