Skip to content

Commit 81cdd26

Browse files
jenspinneysykesm
authored andcommittedSep 16, 2015
Add encryption package
[#103024166] Signed-off-by: Matthew Sykes <matthew.sykes@gmail.com>
1 parent 86e9b5d commit 81cdd26

8 files changed

+540
-0
lines changed
 

‎encryption/crypt.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package encryption
2+
3+
import (
4+
"crypto/cipher"
5+
"errors"
6+
"fmt"
7+
"io"
8+
)
9+
10+
type Encrypted struct {
11+
Nonce []byte
12+
KeyLabel string
13+
CipherText []byte
14+
}
15+
16+
type Encryptor interface {
17+
Encrypt(plaintext []byte) (Encrypted, error)
18+
}
19+
20+
type Decryptor interface {
21+
Decrypt(encrypted Encrypted) ([]byte, error)
22+
}
23+
24+
type Cryptor interface {
25+
Encryptor
26+
Decryptor
27+
}
28+
29+
type cryptor struct {
30+
keyManager KeyManager
31+
prng io.Reader
32+
}
33+
34+
func NewCryptor(keyManager KeyManager, prng io.Reader) Cryptor {
35+
return &cryptor{
36+
keyManager: keyManager,
37+
prng: prng,
38+
}
39+
}
40+
41+
func (c *cryptor) Encrypt(plaintext []byte) (Encrypted, error) {
42+
key := c.keyManager.EncryptionKey()
43+
44+
aead, err := cipher.NewGCM(key.Block())
45+
if err != nil {
46+
return Encrypted{}, fmt.Errorf("Unable to create GCM-wrapped cipher: %q", err)
47+
}
48+
49+
nonce := make([]byte, aead.NonceSize())
50+
n, err := c.prng.Read(nonce)
51+
if err != nil {
52+
return Encrypted{}, fmt.Errorf("Unable to generate random nonce: %q", err)
53+
}
54+
if n != len(nonce) {
55+
return Encrypted{}, errors.New("Unable to generate random nonce")
56+
}
57+
58+
ciphertext := aead.Seal(nil, nonce, plaintext, nil)
59+
return Encrypted{KeyLabel: key.Label(), Nonce: nonce, CipherText: ciphertext}, nil
60+
}
61+
62+
func (d *cryptor) Decrypt(encrypted Encrypted) ([]byte, error) {
63+
key := d.keyManager.DecryptionKey(encrypted.KeyLabel)
64+
if key == nil {
65+
return nil, fmt.Errorf("Key with label %q was not found", encrypted.KeyLabel)
66+
}
67+
68+
aead, err := cipher.NewGCM(key.Block())
69+
if err != nil {
70+
return nil, fmt.Errorf("Unable to create GCM-wrapped cipher: %q", err)
71+
}
72+
73+
return aead.Open(nil, encrypted.Nonce, encrypted.CipherText, nil)
74+
}

‎encryption/crypt_test.go

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package encryption_test
2+
3+
import (
4+
"bytes"
5+
"crypto/des"
6+
"crypto/rand"
7+
"io"
8+
9+
"github.com/cloudfoundry-incubator/bbs/encryption"
10+
"github.com/cloudfoundry-incubator/bbs/encryption/fakes"
11+
12+
. "github.com/onsi/ginkgo"
13+
. "github.com/onsi/gomega"
14+
)
15+
16+
var _ = Describe("Crypt", func() {
17+
var cryptor encryption.Cryptor
18+
var keyManager encryption.KeyManager
19+
var prng io.Reader
20+
21+
BeforeEach(func() {
22+
key, err := encryption.NewKey("label", "pass phrase")
23+
Expect(err).NotTo(HaveOccurred())
24+
keyManager, err = encryption.NewKeyManager(key, nil)
25+
Expect(err).NotTo(HaveOccurred())
26+
prng = rand.Reader
27+
})
28+
29+
JustBeforeEach(func() {
30+
cryptor = encryption.NewCryptor(keyManager, prng)
31+
})
32+
33+
It("successfully encrypts and decrypts with a key", func() {
34+
input := []byte("some plaintext data")
35+
36+
encrypted, err := cryptor.Encrypt(input)
37+
Expect(err).NotTo(HaveOccurred())
38+
Expect(encrypted.CipherText).NotTo(HaveLen(0))
39+
Expect(encrypted.CipherText).NotTo(Equal(input))
40+
41+
plaintext, err := cryptor.Decrypt(encrypted)
42+
Expect(err).NotTo(HaveOccurred())
43+
Expect(plaintext).NotTo(HaveLen(0))
44+
Expect(plaintext).To(Equal(input))
45+
})
46+
47+
Context("when the nonce is incorrect", func() {
48+
It("fails to decrypt", func() {
49+
input := []byte("some plaintext data")
50+
51+
encrypted, err := cryptor.Encrypt(input)
52+
Expect(err).NotTo(HaveOccurred())
53+
Expect(encrypted.CipherText).NotTo(HaveLen(0))
54+
Expect(encrypted.CipherText).NotTo(Equal(input))
55+
56+
encrypted.Nonce = []byte("123456789012")
57+
58+
_, err = cryptor.Decrypt(encrypted)
59+
Expect(err).To(HaveOccurred())
60+
Expect(err).To(MatchError("cipher: message authentication failed"))
61+
})
62+
})
63+
64+
Context("when the key is not found", func() {
65+
It("fails to decrypt", func() {
66+
encrypted := encryption.Encrypted{
67+
KeyLabel: "doesnt-exist",
68+
Nonce: []byte("123456789012"),
69+
}
70+
71+
_, err := cryptor.Decrypt(encrypted)
72+
Expect(err).To(HaveOccurred())
73+
Expect(err).To(MatchError(`Key with label "doesnt-exist" was not found`))
74+
})
75+
})
76+
77+
Context("when the ciphertext is modified", func() {
78+
It("fails to decrypt", func() {
79+
input := []byte("some plaintext data")
80+
81+
encrypted, err := cryptor.Encrypt(input)
82+
Expect(err).NotTo(HaveOccurred())
83+
Expect(encrypted.CipherText).NotTo(HaveLen(0))
84+
Expect(encrypted.CipherText).NotTo(Equal(input))
85+
86+
encrypted.CipherText[0] ^= 1
87+
88+
_, err = cryptor.Decrypt(encrypted)
89+
Expect(err).To(HaveOccurred())
90+
Expect(err).To(MatchError("cipher: message authentication failed"))
91+
})
92+
})
93+
94+
Context("when the random number generator fails", func() {
95+
BeforeEach(func() {
96+
prng = bytes.NewBuffer([]byte{})
97+
})
98+
99+
It("fails to encrypt", func() {
100+
input := []byte("some plaintext data")
101+
102+
_, err := cryptor.Encrypt(input)
103+
Expect(err).To(MatchError(`Unable to generate random nonce: "EOF"`))
104+
})
105+
})
106+
107+
Context("when the random number generator fails to generate enough data", func() {
108+
BeforeEach(func() {
109+
prng = bytes.NewBufferString("goo")
110+
})
111+
112+
It("fails to encrypt", func() {
113+
input := []byte("some plaintext data")
114+
115+
_, err := cryptor.Encrypt(input)
116+
Expect(err).To(MatchError("Unable to generate random nonce"))
117+
})
118+
})
119+
120+
Context("when the encryption key is invalid", func() {
121+
var key *fakes.FakeKey
122+
123+
BeforeEach(func() {
124+
desCipher, err := des.NewCipher([]byte("12345678"))
125+
Expect(err).NotTo(HaveOccurred())
126+
127+
key = &fakes.FakeKey{}
128+
key.BlockReturns(desCipher)
129+
keyManager, err = encryption.NewKeyManager(key, nil)
130+
Expect(err).NotTo(HaveOccurred())
131+
})
132+
133+
It("returns an error", func() {
134+
input := []byte("some plaintext data")
135+
136+
_, err := cryptor.Encrypt(input)
137+
Expect(err).To(MatchError(HavePrefix("Unable to create GCM-wrapped cipher:")))
138+
})
139+
})
140+
})

‎encryption/encryption_suite_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package encryption_test
2+
3+
import (
4+
. "github.com/onsi/ginkgo"
5+
. "github.com/onsi/gomega"
6+
7+
"testing"
8+
)
9+
10+
func TestEncryption(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Encryption Suite")
13+
}

‎encryption/fakes/fake_key.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// This file was generated by counterfeiter
2+
package fakes
3+
4+
import (
5+
"crypto/cipher"
6+
"sync"
7+
8+
"github.com/cloudfoundry-incubator/bbs/encryption"
9+
)
10+
11+
type FakeKey struct {
12+
LabelStub func() string
13+
labelMutex sync.RWMutex
14+
labelArgsForCall []struct{}
15+
labelReturns struct {
16+
result1 string
17+
}
18+
BlockStub func() cipher.Block
19+
blockMutex sync.RWMutex
20+
blockArgsForCall []struct{}
21+
blockReturns struct {
22+
result1 cipher.Block
23+
}
24+
}
25+
26+
func (fake *FakeKey) Label() string {
27+
fake.labelMutex.Lock()
28+
fake.labelArgsForCall = append(fake.labelArgsForCall, struct{}{})
29+
fake.labelMutex.Unlock()
30+
if fake.LabelStub != nil {
31+
return fake.LabelStub()
32+
} else {
33+
return fake.labelReturns.result1
34+
}
35+
}
36+
37+
func (fake *FakeKey) LabelCallCount() int {
38+
fake.labelMutex.RLock()
39+
defer fake.labelMutex.RUnlock()
40+
return len(fake.labelArgsForCall)
41+
}
42+
43+
func (fake *FakeKey) LabelReturns(result1 string) {
44+
fake.LabelStub = nil
45+
fake.labelReturns = struct {
46+
result1 string
47+
}{result1}
48+
}
49+
50+
func (fake *FakeKey) Block() cipher.Block {
51+
fake.blockMutex.Lock()
52+
fake.blockArgsForCall = append(fake.blockArgsForCall, struct{}{})
53+
fake.blockMutex.Unlock()
54+
if fake.BlockStub != nil {
55+
return fake.BlockStub()
56+
} else {
57+
return fake.blockReturns.result1
58+
}
59+
}
60+
61+
func (fake *FakeKey) BlockCallCount() int {
62+
fake.blockMutex.RLock()
63+
defer fake.blockMutex.RUnlock()
64+
return len(fake.blockArgsForCall)
65+
}
66+
67+
func (fake *FakeKey) BlockReturns(result1 cipher.Block) {
68+
fake.BlockStub = nil
69+
fake.blockReturns = struct {
70+
result1 cipher.Block
71+
}{result1}
72+
}
73+
74+
var _ encryption.Key = new(FakeKey)

‎encryption/key.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package encryption
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"crypto/sha256"
7+
"errors"
8+
)
9+
10+
//go:generate counterfeiter . Key
11+
type Key interface {
12+
Label() string
13+
Block() cipher.Block
14+
}
15+
16+
type key struct {
17+
block cipher.Block
18+
label string
19+
}
20+
21+
func NewKey(label, phrase string) (Key, error) {
22+
if label == "" {
23+
return nil, errors.New("A key label is required")
24+
}
25+
26+
hash := sha256.Sum256([]byte(phrase))
27+
block, err := aes.NewCipher(hash[:])
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
return &key{
33+
label: label,
34+
block: block,
35+
}, nil
36+
}
37+
38+
func (k *key) Label() string {
39+
return k.label
40+
}
41+
42+
func (k *key) Block() cipher.Block {
43+
return k.block
44+
}

‎encryption/key_manager.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package encryption
2+
3+
import "fmt"
4+
5+
type keyManager struct {
6+
encryptionKey Key
7+
decryptionKeys map[string]Key
8+
}
9+
10+
type KeyManager interface {
11+
EncryptionKey() Key
12+
DecryptionKey(label string) Key
13+
}
14+
15+
func NewKeyManager(encryptionKey Key, decryptionKeys []Key) (KeyManager, error) {
16+
decryptionKeyMap := map[string]Key{
17+
encryptionKey.Label(): encryptionKey,
18+
}
19+
20+
for _, key := range decryptionKeys {
21+
if existingKey, ok := decryptionKeyMap[key.Label()]; ok && key != existingKey {
22+
return nil, fmt.Errorf("Multiple keys with the same label: %q", key.Label())
23+
}
24+
decryptionKeyMap[key.Label()] = key
25+
}
26+
27+
return &keyManager{
28+
encryptionKey: encryptionKey,
29+
decryptionKeys: decryptionKeyMap,
30+
}, nil
31+
}
32+
33+
func (m *keyManager) EncryptionKey() Key {
34+
return m.encryptionKey
35+
}
36+
37+
func (m *keyManager) DecryptionKey(label string) Key {
38+
return m.decryptionKeys[label]
39+
}

‎encryption/key_manager_test.go

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package encryption_test
2+
3+
import (
4+
"github.com/cloudfoundry-incubator/bbs/encryption"
5+
6+
. "github.com/onsi/ginkgo"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
var _ = Describe("KeyManager", func() {
11+
var (
12+
encryptionKey encryption.Key
13+
decryptionKeys []encryption.Key
14+
manager encryption.KeyManager
15+
cerr error
16+
)
17+
18+
BeforeEach(func() {
19+
var err error
20+
encryptionKey, err = encryption.NewKey("key label", "pass phrase")
21+
Expect(err).NotTo(HaveOccurred())
22+
decryptionKeys = []encryption.Key{}
23+
cerr = nil
24+
})
25+
26+
JustBeforeEach(func() {
27+
manager, cerr = encryption.NewKeyManager(encryptionKey, decryptionKeys)
28+
})
29+
30+
It("stores the correct encryption key", func() {
31+
Expect(cerr).NotTo(HaveOccurred())
32+
Expect(manager.EncryptionKey()).To(Equal(encryptionKey))
33+
})
34+
35+
It("adds the encryption key as a decryption key", func() {
36+
Expect(manager.DecryptionKey(encryptionKey.Label())).To(Equal(manager.EncryptionKey()))
37+
})
38+
39+
Context("when decryption keys are provided", func() {
40+
BeforeEach(func() {
41+
key1, err := encryption.NewKey("label1", "some pass phrase")
42+
Expect(err).NotTo(HaveOccurred())
43+
44+
key2, err := encryption.NewKey("label2", "some other pass phrase")
45+
Expect(err).NotTo(HaveOccurred())
46+
47+
key3, err := encryption.NewKey("label3", "")
48+
Expect(err).NotTo(HaveOccurred())
49+
50+
decryptionKeys = []encryption.Key{key1, key2, key3}
51+
})
52+
53+
It("stores the decryption keys", func() {
54+
for _, decryptionKey := range decryptionKeys {
55+
key := manager.DecryptionKey(decryptionKey.Label())
56+
Expect(key).To(Equal(decryptionKey))
57+
}
58+
})
59+
60+
It("includes the encryption key", func() {
61+
key := manager.DecryptionKey(encryptionKey.Label())
62+
Expect(key).To(Equal(manager.EncryptionKey()))
63+
})
64+
})
65+
66+
Context("when the encryption key and a decryption key have the same label but different blocks", func() {
67+
BeforeEach(func() {
68+
decryptKey, err := encryption.NewKey("key label", "a different pass phrase")
69+
Expect(err).NotTo(HaveOccurred())
70+
71+
decryptionKeys = []encryption.Key{decryptKey}
72+
})
73+
74+
It("returns a multiple key error", func() {
75+
Expect(cerr).To(MatchError(`Multiple keys with the same label: "key label"`))
76+
})
77+
})
78+
79+
Context("when different keys with the same label are provided in decryption keys", func() {
80+
BeforeEach(func() {
81+
duplicateLabelKey1, err := encryption.NewKey("label", "a pass phrase")
82+
duplicateLabelKey2, err := encryption.NewKey("label", "a different pass phrase")
83+
Expect(err).NotTo(HaveOccurred())
84+
85+
decryptionKeys = []encryption.Key{duplicateLabelKey1, duplicateLabelKey2}
86+
})
87+
88+
It("returns a multiple key error", func() {
89+
Expect(cerr).To(MatchError(`Multiple keys with the same label: "label"`))
90+
})
91+
})
92+
93+
Context("when attempting to retrieve a key that does not exist", func() {
94+
It("returns nil", func() {
95+
key := manager.DecryptionKey("bogus")
96+
Expect(key).To(BeNil())
97+
})
98+
})
99+
100+
Context("when no decryption keys are provided", func() {
101+
BeforeEach(func() {
102+
decryptionKeys = nil
103+
})
104+
105+
It("still includes the encryption key", func() {
106+
key := manager.DecryptionKey(encryptionKey.Label())
107+
Expect(key).To(Equal(encryptionKey))
108+
})
109+
})
110+
})

‎encryption/key_test.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package encryption_test
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/sha256"
6+
"fmt"
7+
8+
"github.com/cloudfoundry-incubator/bbs/encryption"
9+
10+
. "github.com/onsi/ginkgo"
11+
. "github.com/onsi/gomega"
12+
)
13+
14+
var _ = Describe("Key", func() {
15+
Describe("NewKey", func() {
16+
It("generates a 256 bit key from a string that can be used as aes keys", func() {
17+
phrases := []string{
18+
"",
19+
"a",
20+
"a short phrase",
21+
"12345678901234567890123456789012",
22+
"1234567890123456789012345678901234567890123456789012345678901234567890",
23+
}
24+
25+
for i, phrase := range phrases {
26+
label := fmt.Sprintf("%d", i)
27+
key, err := encryption.NewKey(label, phrase)
28+
Expect(err).NotTo(HaveOccurred())
29+
Expect(key.Label()).To(Equal(label))
30+
Expect(key.Block().BlockSize()).To(Equal(aes.BlockSize))
31+
32+
phraseHash := sha256.Sum256([]byte(phrase))
33+
block, err := aes.NewCipher(phraseHash[:])
34+
Expect(err).NotTo(HaveOccurred())
35+
Expect(key.Block()).To(Equal(block))
36+
}
37+
})
38+
39+
Context("when a key label is not specified", func() {
40+
It("returns a meaningful error", func() {
41+
_, err := encryption.NewKey("", "phrase")
42+
Expect(err).To(MatchError("A key label is required"))
43+
})
44+
})
45+
})
46+
})

0 commit comments

Comments
 (0)
Please sign in to comment.