-
Notifications
You must be signed in to change notification settings - Fork 656
[encryption] NaCL/secretbox + wrapper around etcd wal + snapshot #1701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2df311b
ec53416
3e2cebe
6917257
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| package encryption | ||
|
|
||
| import ( | ||
| "crypto/rand" | ||
| "encoding/base64" | ||
| "fmt" | ||
| "io" | ||
| "strings" | ||
|
|
||
| "github.com/docker/swarmkit/api" | ||
| "github.com/gogo/protobuf/proto" | ||
| "github.com/pkg/errors" | ||
| ) | ||
|
|
||
| // This package defines the interfaces and encryption package | ||
|
|
||
| const humanReadablePrefix = "SWMKEY-1-" | ||
|
|
||
| // ErrCannotDecrypt is the type of error returned when some data cannot be decryptd as plaintext | ||
| type ErrCannotDecrypt struct { | ||
| msg string | ||
| } | ||
|
|
||
| func (e ErrCannotDecrypt) Error() string { | ||
| return e.msg | ||
| } | ||
|
|
||
| // A Decrypter can decrypt an encrypted record | ||
| type Decrypter interface { | ||
| Decrypt(api.MaybeEncryptedRecord) ([]byte, error) | ||
| } | ||
|
|
||
| // A Encrypter can encrypt some bytes into an encrypted record | ||
| type Encrypter interface { | ||
| Encrypt(data []byte) (*api.MaybeEncryptedRecord, error) | ||
| } | ||
|
|
||
| type noopCrypter struct{} | ||
|
|
||
| func (n noopCrypter) Decrypt(e api.MaybeEncryptedRecord) ([]byte, error) { | ||
| if e.Algorithm != n.Algorithm() { | ||
| return nil, fmt.Errorf("record is encrypted") | ||
| } | ||
| return e.Data, nil | ||
| } | ||
|
|
||
| func (n noopCrypter) Encrypt(data []byte) (*api.MaybeEncryptedRecord, error) { | ||
| return &api.MaybeEncryptedRecord{ | ||
| Algorithm: n.Algorithm(), | ||
| Data: data, | ||
| }, nil | ||
| } | ||
|
|
||
| func (n noopCrypter) Algorithm() api.MaybeEncryptedRecord_Algorithm { | ||
| return api.MaybeEncryptedRecord_NotEncrypted | ||
| } | ||
|
|
||
| // NoopCrypter is just a pass-through crypter - it does not actually encrypt or | ||
| // decrypt any data | ||
| var NoopCrypter = noopCrypter{} | ||
|
|
||
| // Decrypt turns a slice of bytes serialized as an MaybeEncryptedRecord into a slice of plaintext bytes | ||
| func Decrypt(encryptd []byte, decrypter Decrypter) ([]byte, error) { | ||
| if decrypter == nil { | ||
| return nil, ErrCannotDecrypt{msg: "no decrypter specified"} | ||
| } | ||
| r := api.MaybeEncryptedRecord{} | ||
| if err := proto.Unmarshal(encryptd, &r); err != nil { | ||
| // nope, this wasn't marshalled as a MaybeEncryptedRecord | ||
| return nil, ErrCannotDecrypt{msg: "unable to unmarshal as MaybeEncryptedRecord"} | ||
| } | ||
| plaintext, err := decrypter.Decrypt(r) | ||
| if err != nil { | ||
| return nil, ErrCannotDecrypt{msg: err.Error()} | ||
| } | ||
| return plaintext, nil | ||
| } | ||
|
|
||
| // Encrypt turns a slice of bytes into a serialized MaybeEncryptedRecord slice of bytes | ||
| func Encrypt(plaintext []byte, encrypter Encrypter) ([]byte, error) { | ||
| if encrypter == nil { | ||
| return nil, fmt.Errorf("no encrypter specified") | ||
| } | ||
|
|
||
| encryptedRecord, err := encrypter.Encrypt(plaintext) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "unable to encrypt data") | ||
| } | ||
|
|
||
| data, err := proto.Marshal(encryptedRecord) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "unable to marshal as MaybeEncryptedRecord") | ||
| } | ||
|
|
||
| return data, nil | ||
| } | ||
|
|
||
| // Defaults returns a default encrypter and decrypter | ||
| func Defaults(key []byte) (Encrypter, Decrypter) { | ||
| n := NewNACLSecretbox(key) | ||
| return n, n | ||
| } | ||
|
|
||
| // GenerateSecretKey generates a secret key that can be used for encrypting data | ||
| // using this package | ||
| func GenerateSecretKey() []byte { | ||
| secretData := make([]byte, naclSecretboxKeySize) | ||
| if _, err := io.ReadFull(rand.Reader, secretData); err != nil { | ||
| // panic if we can't read random data | ||
| panic(errors.Wrap(err, "failed to read random bytes")) | ||
| } | ||
| return secretData | ||
| } | ||
|
|
||
| // HumanReadableKey displays a secret key in a human readable way | ||
| func HumanReadableKey(key []byte) string { | ||
| // base64-encode the key | ||
| return humanReadablePrefix + base64.StdEncoding.EncodeToString(key) | ||
| } | ||
|
|
||
| // ParseHumanReadableKey returns a key as bytes from recognized serializations of | ||
| // said keys | ||
| func ParseHumanReadableKey(key string) ([]byte, error) { | ||
| if !strings.HasPrefix(key, humanReadablePrefix) { | ||
| return nil, fmt.Errorf("invalid key string") | ||
| } | ||
| keyBytes, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(key, humanReadablePrefix)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("invalid key string") | ||
| } | ||
| return keyBytes, nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| package encryption | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestEncryptDecrypt(t *testing.T) { | ||
| // not providing an encrypter will fail | ||
| msg := []byte("hello again swarmkit") | ||
| _, err := Encrypt(msg, nil) | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), "no encrypter") | ||
|
|
||
| // noop encrypter can encrypt | ||
| encrypted, err := Encrypt(msg, NoopCrypter) | ||
| require.NoError(t, err) | ||
|
|
||
| // not providing a decrypter will fail | ||
| _, err = Decrypt(encrypted, nil) | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), "no decrypter") | ||
|
|
||
| // noop decrypter can decrypt | ||
| decrypted, err := Decrypt(encrypted, NoopCrypter) | ||
| require.NoError(t, err) | ||
| require.Equal(t, msg, decrypted) | ||
|
|
||
| // the default encrypter can produce something the default decrypter can read | ||
| encrypter, decrypter := Defaults([]byte("key")) | ||
| encrypted, err = Encrypt(msg, encrypter) | ||
| require.NoError(t, err) | ||
| decrypted, err = Decrypt(encrypted, decrypter) | ||
| require.NoError(t, err) | ||
| require.Equal(t, msg, decrypted) | ||
|
|
||
| // mismatched encrypters and decrypters can't read the content produced by each | ||
| encrypted, err = Encrypt(msg, NoopCrypter) | ||
| require.NoError(t, err) | ||
| _, err = Decrypt(encrypted, decrypter) | ||
| require.Error(t, err) | ||
| require.IsType(t, ErrCannotDecrypt{}, err) | ||
|
|
||
| encrypted, err = Encrypt(msg, encrypter) | ||
| require.NoError(t, err) | ||
| _, err = Decrypt(encrypted, NoopCrypter) | ||
| require.Error(t, err) | ||
| require.IsType(t, ErrCannotDecrypt{}, err) | ||
| } | ||
|
|
||
| func TestHumanReadable(t *testing.T) { | ||
| // we can produce human readable strings that can then be re-parsed | ||
| key := GenerateSecretKey() | ||
| keyString := HumanReadableKey(key) | ||
| parsedKey, err := ParseHumanReadableKey(keyString) | ||
| require.NoError(t, err) | ||
| require.Equal(t, parsedKey, key) | ||
|
|
||
| // if the prefix is wrong, we can't parse the key | ||
| _, err = ParseHumanReadableKey("A" + keyString) | ||
| require.Error(t, err) | ||
|
|
||
| // With the right prefix, we can't parse if the key isn't base64 encoded | ||
| _, err = ParseHumanReadableKey(humanReadablePrefix + "aaaaa/") | ||
| require.Error(t, err) | ||
|
|
||
| // Extra padding also fails | ||
| _, err = ParseHumanReadableKey(keyString + "=") | ||
| require.Error(t, err) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package encryption | ||
|
|
||
| import ( | ||
| "crypto/rand" | ||
| "fmt" | ||
| "io" | ||
|
|
||
| "github.com/docker/swarmkit/api" | ||
|
|
||
| "golang.org/x/crypto/nacl/secretbox" | ||
| ) | ||
|
|
||
| const naclSecretboxKeySize = 32 | ||
| const naclSecretboxNonceSize = 24 | ||
|
|
||
| // This provides the default implementation of an encrypter and decrypter, as well | ||
| // as the default KDF function. | ||
|
|
||
| // NACLSecretbox is an implementation of an encrypter/decrypter. Encrypting | ||
| // generates random Nonces. | ||
| type NACLSecretbox struct { | ||
| key [naclSecretboxKeySize]byte | ||
| } | ||
|
|
||
| // NewNACLSecretbox returns a new NACL secretbox encrypter/decrypter with the given key | ||
| func NewNACLSecretbox(key []byte) NACLSecretbox { | ||
| secretbox := NACLSecretbox{} | ||
| copy(secretbox.key[:], key) | ||
| return secretbox | ||
| } | ||
|
|
||
| // Algorithm returns the type of algorhtm this is (NACL Secretbox using XSalsa20 and Poly1305) | ||
| func (n NACLSecretbox) Algorithm() api.MaybeEncryptedRecord_Algorithm { | ||
| return api.MaybeEncryptedRecord_NACLSecretboxSalsa20Poly1305 | ||
| } | ||
|
|
||
| // Encrypt encrypts some bytes and returns an encrypted record | ||
| func (n NACLSecretbox) Encrypt(data []byte) (*api.MaybeEncryptedRecord, error) { | ||
| var nonce [24]byte | ||
| if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tab complete. :D Will fix.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed the previous case
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to be clear, both cases need to use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, both use |
||
| return nil, err | ||
| } | ||
|
|
||
| // Seal's first argument is an "out", the data that the new encrypted message should be | ||
| // appended to. Since we don't want to append anything, we pass nil. | ||
| encrypted := secretbox.Seal(nil, data, &nonce, &n.key) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add comment on why the first nil? |
||
| return &api.MaybeEncryptedRecord{ | ||
| Algorithm: n.Algorithm(), | ||
| Data: encrypted, | ||
| Nonce: nonce[:], | ||
| }, nil | ||
| } | ||
|
|
||
| // Decrypt decrypts a MaybeEncryptedRecord and returns some bytes | ||
| func (n NACLSecretbox) Decrypt(record api.MaybeEncryptedRecord) ([]byte, error) { | ||
| if record.Algorithm != n.Algorithm() { | ||
| return nil, fmt.Errorf("not a NACL secretbox record") | ||
| } | ||
| if len(record.Nonce) != naclSecretboxNonceSize { | ||
| return nil, fmt.Errorf("invalid nonce size for NACL secretbox: require 24, got %d", len(record.Nonce)) | ||
| } | ||
|
|
||
| var decryptNonce [naclSecretboxNonceSize]byte | ||
| copy(decryptNonce[:], record.Nonce[:naclSecretboxNonceSize]) | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need a sanity check that |
||
| // Open's first argument is an "out", the data that the decrypted message should be | ||
| // appended to. Since we don't want to append anything, we pass nil. | ||
| decrypted, ok := secretbox.Open(nil, record.Data, &decryptNonce, &n.key) | ||
| if !ok { | ||
| return nil, fmt.Errorf("decryption error using NACL secretbox") | ||
| } | ||
| return decrypted, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| package encryption | ||
|
|
||
| import ( | ||
| "crypto/rand" | ||
| "io" | ||
| "testing" | ||
|
|
||
| "github.com/docker/swarmkit/api" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| // Using the same key to encrypt the same message, this encrypter produces two | ||
| // different ciphertexts because it produces two different nonces. Both | ||
| // of these can be decrypted into the same data though. | ||
| func TestNACLSecretbox(t *testing.T) { | ||
| key := make([]byte, 32) | ||
| _, err := io.ReadFull(rand.Reader, key) | ||
| require.NoError(t, err) | ||
|
|
||
| crypter := NewNACLSecretbox(key) | ||
| data := []byte("Hello again world") | ||
|
|
||
| er1, err := crypter.Encrypt(data) | ||
| require.NoError(t, err) | ||
|
|
||
| er2, err := crypter.Encrypt(data) | ||
| require.NoError(t, err) | ||
|
|
||
| require.NotEqual(t, er1.Data, er2.Data) | ||
| require.NotEmpty(t, er1.Nonce, er2.Nonce) | ||
|
|
||
| result, err := crypter.Decrypt(*er1) | ||
| require.NoError(t, err) | ||
| require.Equal(t, data, result) | ||
|
|
||
| result, err = crypter.Decrypt(*er2) | ||
| require.NoError(t, err) | ||
| require.Equal(t, data, result) | ||
| } | ||
|
|
||
| func TestNACLSecretboxInvalidAlgorithm(t *testing.T) { | ||
| key := make([]byte, 32) | ||
| _, err := io.ReadFull(rand.Reader, key) | ||
| require.NoError(t, err) | ||
|
|
||
| crypter := NewNACLSecretbox(key) | ||
| er, err := crypter.Encrypt([]byte("Hello again world")) | ||
| require.NoError(t, err) | ||
| er.Algorithm = api.MaybeEncryptedRecord_NotEncrypted | ||
|
|
||
| _, err = crypter.Decrypt(*er) | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), "not a NACL secretbox") | ||
| } | ||
|
|
||
| func TestNACLSecretboxCannotDecryptWithoutRightKey(t *testing.T) { | ||
| key := make([]byte, 32) | ||
| _, err := io.ReadFull(rand.Reader, key) | ||
| require.NoError(t, err) | ||
|
|
||
| crypter := NewNACLSecretbox(key) | ||
| er, err := crypter.Encrypt([]byte("Hello again world")) | ||
| require.NoError(t, err) | ||
|
|
||
| crypter = NewNACLSecretbox([]byte{}) | ||
| _, err = crypter.Decrypt(*er) | ||
| require.Error(t, err) | ||
| } | ||
|
|
||
| func TestNACLSecretboxInvalidNonce(t *testing.T) { | ||
| key := make([]byte, 32) | ||
| _, err := io.ReadFull(rand.Reader, key) | ||
| require.NoError(t, err) | ||
|
|
||
| crypter := NewNACLSecretbox(key) | ||
| er, err := crypter.Encrypt([]byte("Hello again world")) | ||
| require.NoError(t, err) | ||
| er.Nonce = er.Nonce[:20] | ||
|
|
||
| _, err = crypter.Decrypt(*er) | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), "invalid nonce size") | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is NONE for backwards compatibility, or have we stepped away from "encrypted by default, just sometimes with the key on disk"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
NONEis sometimes used for encrypting the DEK only (it may optionally be encrypted with a KEK)