Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
740 changes: 502 additions & 238 deletions api/types.pb.go

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions api/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -840,3 +840,14 @@ message HealthConfig {
// container as unhealthy. Zero means inherit.
int32 retries = 4;
}

message MaybeEncryptedRecord {
enum Algorithm {
NONE = 0 [(gogoproto.enumvalue_customname) = "NotEncrypted"];
SECRETBOX_SALSA20_POLY1305 = 1 [(gogoproto.enumvalue_customname) = "NACLSecretboxSalsa20Poly1305"];
}
Copy link
Copy Markdown

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"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NONE is sometimes used for encrypting the DEK only (it may optionally be encrypted with a KEK)


Algorithm algorithm = 1;
bytes data = 2;
bytes nonce = 3;
}
132 changes: 132 additions & 0 deletions manager/encryption/encryption.go
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
}
71 changes: 71 additions & 0 deletions manager/encryption/encryption_test.go
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)
}
73 changes: 73 additions & 0 deletions manager/encryption/nacl.go
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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why ReadFull here and Read above?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tab complete. :D Will fix.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the previous case

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be clear, both cases need to use io.ReadFull.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, both use io.ReadFull now.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need a sanity check that record.Nonce has at least 24 bytes?

// 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
}
83 changes: 83 additions & 0 deletions manager/encryption/nacl_test.go
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")
}
Loading