Skip to content

Commit

Permalink
Merge pull request #2 from saylorsolutions/feature/passlock
Browse files Browse the repository at this point in the history
Added password-based encryption, and the structure for using multiple "surrogate" keys to interact with the same encrypted payload.
  • Loading branch information
drognisep authored Aug 6, 2023
2 parents d31f03b + bf9c07e commit 6351498
Show file tree
Hide file tree
Showing 10 changed files with 859 additions and 2 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 6 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ module github.com/saylorsolutions/gocryptx

go 1.19

require github.com/stretchr/testify v1.8.4
require (
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
)

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
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ 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/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=
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=
Expand Down
53 changes: 53 additions & 0 deletions pkg/passlock/doc.go
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
193 changes: 193 additions & 0 deletions pkg/passlock/key.go
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
}
68 changes: 68 additions & 0 deletions pkg/passlock/key_test.go
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)
}
Loading

0 comments on commit 6351498

Please sign in to comment.