Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ This repo provides a list of utility modules for common crypto operations.

### AES

- factory methods to construct an AES-GCM cipher with a 96-bit nonce from the input raw key bytes
- factory methods to construct an AES-GCM cipher with a 96-bit nonce from the input raw key bytes or hex text
- encrypt & decrypt methods, the output ciphertext is prefixed with the random nonce.
- verify the constructed cipher against the check value

### DES

Expand All @@ -13,7 +14,7 @@ This repo provides a list of utility modules for common crypto operations.

### KEK Bundle

Helper class to construct a 3DES key encryption key from a list of components.
Helper class to construct a TripleDES or AES key encryption key from a list of components.

### RSA

Expand Down
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.11.0
1.12.0
40 changes: 23 additions & 17 deletions aes/aes_cipher.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,27 @@ package aes
import (
"crypto/aes"
"crypto/cipher"
"encoding/hex"
"errors"
"strings"

"github.com/hashicorp/go-uuid"
)

const checkValueBytes = 3

var keyCheckValuePlainText16Bytes = make([]byte, 16)

// Cipher is wrapper of the AES GCM cipher and stores the raw key bytes
type Cipher struct {
gcm cipher.AEAD
KeyBytes []byte
}

// New constructs a new AES GCM cipher using the raw key bytes provided, the raw bytes must be
// either 16, 24, or 32 bytes
// CreateFromKeyBytes constructs a new AES GCM cipher using the raw key bytes provided, the raw bytes must be either 16, 24, or 32 bytes.
Comment thread
lwschan-tw marked this conversation as resolved.
Outdated
// Deprecated: Use CreateFromKeyBytes instead.
func New(keyBytes []byte) (Cipher, error) {
var err error

// Setup the cipher
aesCipher, err := aes.NewCipher(keyBytes)
if err != nil {
return Cipher{}, err
}

// Setup the GCM
gcmCipher, err := cipher.NewGCM(aesCipher)
if err != nil {
return Cipher{}, err
}

return Cipher{gcmCipher, keyBytes}, nil
return CreateFromKeyBytes(keyBytes)
}

// Encrypt takes plain bytes and output cipher bytes, the nonce will be prefixed to
Expand Down Expand Up @@ -73,3 +65,17 @@ func (cipher *Cipher) Decrypt(cipherBytes []byte, nonce []byte) ([]byte, error)

return cipher.gcm.Open(nil, nonce, cipherBytes, nil)
}

func (c *Cipher) CheckValue() string {
block, err := aes.NewCipher(c.KeyBytes)
if err != nil {
return ""
}
cipherBytes := make([]byte, len(keyCheckValuePlainText16Bytes))
block.Encrypt(cipherBytes, keyCheckValuePlainText16Bytes)
return hex.EncodeToString(cipherBytes[:checkValueBytes])
}

func (c *Cipher) VerifyCheckValue(checkValue string) bool {
return strings.EqualFold(c.CheckValue(), checkValue)
Comment thread
lwschan-tw marked this conversation as resolved.
Outdated
}
28 changes: 28 additions & 0 deletions aes/aes_cipher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,34 @@ func TestAESCipher_EncryptAndDecryptNotPrefixNonce(t *testing.T) {
}
}

func TestAESCheckValue(t *testing.T) {
keyBytes, _ := hex.DecodeString("2b7e151628aed2a6abf7158809cf4f3c")
cipher, _ := New(keyBytes)

Comment thread
lwschan-tw marked this conversation as resolved.
checkValue := cipher.CheckValue()
if checkValue != "7df76b" {
t.Errorf("Expected check value 7df76b but got %s", checkValue)
}
}

func TestAESVerifyCheckValue(t *testing.T) {
keyBytes, _ := hex.DecodeString("2b7e151628aed2a6abf7158809cf4f3c")
cipher, _ := New(keyBytes)

if !cipher.VerifyCheckValue("7df76b") {
t.Error("Expected check value to verify successfully")
}
if !cipher.VerifyCheckValue("7DF76B") {
t.Error("Expected check value verification to be case insensitive")
}
if cipher.VerifyCheckValue("abcdef") {
t.Error("Expected wrong check value to fail verification")
}
if cipher.VerifyCheckValue("") {
t.Error("Expected empty check value to fail verification")
}
}

func TestAESCipher_EncryptAndDecryptPrefixNonce(t *testing.T) {
keyBytes, _ := uuid.GenerateRandomBytes(32)
cipher, _ := New(keyBytes)
Expand Down
49 changes: 49 additions & 0 deletions aes/aes_factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package aes

import (
"crypto/aes"
"crypto/cipher"
"encoding/hex"
"errors"
)

// CreateFromKeyBytes constructs a new AES GCM cipher using the raw key bytes provided, the raw bytes must be either 16, 24, or 32 bytes
func CreateFromKeyBytes(keyBytes []byte) (Cipher, error) {
if len(keyBytes) != 16 && len(keyBytes) != 24 && len(keyBytes) != 32 {
return Cipher{}, errors.New("AES key must be 16, 24, or 32 bytes")
}

aesCipher, err := aes.NewCipher(keyBytes)
if err != nil {
return Cipher{}, errors.New("invalid AES keyBlock")
}

gcmCipher, err := cipher.NewGCM(aesCipher)
if err != nil {
return Cipher{}, errors.New("invalid AES GCM cipher")
}

return Cipher{gcmCipher, keyBytes}, nil
}

// CreateFromKeyString constructs a new AES GCM cipher using the hex-encoded key string provided
func CreateFromKeyString(key string) (Cipher, error) {
keyBytes, err := hex.DecodeString(key)
if err != nil {
return Cipher{}, errors.New("AES key is not in correct hex format")
}
return CreateFromKeyBytes(keyBytes)
}
81 changes: 81 additions & 0 deletions aes/aes_factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package aes

import (
"testing"
)

func TestInvalidAESKeyStrings(t *testing.T) {
invalidKeys := []string{
"",
"sme",
"844e5fb5-96d1-4b19-9ce0-b90f252ea370",
" naksn",
"2b7e1516",
"2b7e151628aed2a6abf71588",
}

for _, key := range invalidKeys {
if _, err := CreateFromKeyString(key); err == nil {
t.Errorf("Expecting AES key %s to be invalid", key)
}
}
}

func TestValidAES128KeyStrings(t *testing.T) {
validKeys := []string{
"2b7e151628aed2a6abf7158809cf4f3c",
"000102030405060708090a0b0c0d0e0f",
}

for _, key := range validKeys {
if _, err := CreateFromKeyString(key); err != nil {
t.Errorf("Expecting AES-128 key %s to be valid but got %v", key, err)
}
}
}

func TestValidAES192KeyStrings(t *testing.T) {
validKeys := []string{
"8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b",
}

for _, key := range validKeys {
if _, err := CreateFromKeyString(key); err != nil {
t.Errorf("Expecting AES-192 key %s to be valid but got %v", key, err)
}
}
}

func TestValidAES256KeyStrings(t *testing.T) {
validKeys := []string{
"603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4",
}

for _, key := range validKeys {
if _, err := CreateFromKeyString(key); err != nil {
t.Errorf("Expecting AES-256 key %s to be valid but got %v", key, err)
}
}
}

func TestInvalidAESKeyBytes(t *testing.T) {
invalidLengths := []int{0, 7, 15, 17, 23, 25, 31, 33}

for _, length := range invalidLengths {
keyBytes := make([]byte, length)
if _, err := CreateFromKeyBytes(keyBytes); err == nil {
t.Errorf("Expecting AES key of length %d bytes to be invalid", length)
}
}
}
84 changes: 73 additions & 11 deletions kek/kek_bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,24 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
// package kek helps construct an 3DES key encryption key from a list of components
// package kek helps construct a key encryption key from a list of components
package kek

import (
"errors"

"github.com/hashicorp/vault/sdk/helper/xor"
"github.com/transferwise/crypto/aes"
"github.com/transferwise/crypto/des"
)

type KeyType string

const (
KeyTypeTripleDES KeyType = "TripleDES"
KeyTypeAES KeyType = "AES"
)
Comment thread
lwschan-tw marked this conversation as resolved.

// Bundle is the in memory data structure to help construct a KEK from a list of components
type Bundle struct {
// name of the key
Expand All @@ -29,16 +37,26 @@ type Bundle struct {
Size int
// result key check value
CheckValue string
// key type (TripleDES or AES)
KeyType KeyType
// imported components index value map
Components map[int][]byte
}

// New creates a new KEK Bundle for TripleDES
// Deprecated: Use NewWithKeyType instead
func New(name string, index int, size int, checkValue string) *Bundle {
return NewWithKeyType(name, index, size, checkValue, KeyTypeTripleDES)
}

// NewWithKeyType creates a new KEK Bundle with the specified key type
func NewWithKeyType(name string, index int, size int, checkValue string, keyType KeyType) *Bundle {
return &Bundle{
Name: name,
Index: index,
Size: size,
CheckValue: checkValue,
KeyType: keyType,
Components: make(map[int][]byte),
}
}
Expand All @@ -50,21 +68,38 @@ func (b *Bundle) IsComplete() bool {

// AddComponent add a new component to the Bundle
func (b *Bundle) AddComponent(componentIndex int, componentValue string, componentCheckValue string) error {
cipher, err := des.CreateFromTripleDESKeyString(componentValue)
if err != nil {
return errors.New("invalid component")
}
if !cipher.VerifyCheckValue(componentCheckValue) {
return errors.New("component check value does not tally")
switch b.KeyType {
case KeyTypeAES:
cipher, err := aes.CreateFromKeyString(componentValue)
if err != nil {
return errors.New("invalid component")
}
if !cipher.VerifyCheckValue(componentCheckValue) {
return errors.New("component check value does not tally")
}
b.Components[componentIndex] = cipher.KeyBytes
default:
cipher, err := des.CreateFromTripleDESKeyString(componentValue)
if err != nil {
return errors.New("invalid component")
}
if !cipher.VerifyCheckValue(componentCheckValue) {
return errors.New("component check value does not tally")
}
b.Components[componentIndex] = cipher.KeyBytes
}
Comment thread
lwschan-tw marked this conversation as resolved.

// Override the previous value if the same component is imported again
b.Components[componentIndex] = cipher.KeyBytes
return nil
}

// Merge tries to build the result 3DES key from all the imported components
// Merge tries to build the result TripleDES key from all the imported components
//
// Deprecated: Use MergeTripleDESKey instead.
func (b *Bundle) Merge() (des.Cipher, error) {
return b.MergeTripleDESKey()
}

// MergeTripleDESKey tries to build the result TripleDES key from all the imported components
func (b *Bundle) MergeTripleDESKey() (des.Cipher, error) {
kekBytes := make([]byte, 24)
for _, component := range b.Components {
kekBytes, _ = xor.XORBytes(kekBytes, component)
Expand All @@ -80,3 +115,30 @@ func (b *Bundle) Merge() (des.Cipher, error) {

return kekCipher, nil
}

// MergeAESKey tries to build the result AES key from all the imported components
func (b *Bundle) MergeAESKey() (aes.Cipher, error) {
var keyLen int
for _, component := range b.Components {
keyLen = len(component)
break
}
if keyLen == 0 {
return aes.Cipher{}, errors.New("no components to merge")
}

kekBytes := make([]byte, keyLen)
for _, component := range b.Components {
kekBytes, _ = xor.XORBytes(kekBytes, component)
Comment thread
lwschan-tw marked this conversation as resolved.
Outdated
}
Comment thread
lwschan-tw marked this conversation as resolved.

kekCipher, err := aes.CreateFromKeyBytes(kekBytes)
if err != nil {
return aes.Cipher{}, err
}
if !kekCipher.VerifyCheckValue(b.CheckValue) {
return aes.Cipher{}, errors.New("derived key check value does not tally")
}

return kekCipher, nil
}
Loading