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
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
50 changes: 33 additions & 17 deletions aes/aes_cipher.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,28 @@ 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
// New constructs a new AES GCM cipher using the raw key bytes provided; the raw bytes must be either 16, 24, or 32 bytes.
//
// 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 +66,26 @@ 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 {
if checkValue == "" {
return false
}

derivedCheckValue := c.CheckValue()
if derivedCheckValue == "" {
return false
}

return strings.EqualFold(derivedCheckValue, checkValue)
}
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)
}
}
}
Loading