Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
73 changes: 73 additions & 0 deletions aes/aes_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
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/cipher"
"encoding/hex"
"strings"
)

const (
checkValueDefaultBytes = 3
checkValueMinimumBytes = 2
)

// KCVMethod determines how Key Check Values are computed.
type KCVMethod int

const (
// KCVMethodECB computes KCV by AES-ECB encrypting a block of zero bytes.
KCVMethodECB KCVMethod = iota
// KCVMethodCMAC computes KCV using AES-CMAC over a block of zero bytes per RFC 4493.
KCVMethodCMAC
)

// Key wraps an AES key and supports Key Check Value verification.
type Key struct {
keyBlock cipher.Block
KeyBytes []byte
kcvMethod KCVMethod
}

func (k *Key) VerifyCheckValue(checkValue string) bool {
checkValueBytes := len(checkValue) / 2
if checkValueBytes < checkValueMinimumBytes || checkValueBytes > k.keyBlock.BlockSize() {
return false
}

kcvBytes := k.computeKCV()
derivedCheckValue := hex.EncodeToString(kcvBytes[:checkValueBytes])
return strings.EqualFold(derivedCheckValue, checkValue)
}

func (k *Key) CheckValue() string {
kcvBytes := k.computeKCV()
return hex.EncodeToString(kcvBytes[:checkValueDefaultBytes])
}

func (k *Key) GetKeyBytes() []byte {
return k.KeyBytes
}

func (k *Key) computeKCV() []byte {
zeros := make([]byte, k.keyBlock.BlockSize())

switch k.kcvMethod {
case KCVMethodCMAC:
return cmac(k.keyBlock, zeros)
default:
encrypted := make([]byte, k.keyBlock.BlockSize())
k.keyBlock.Encrypt(encrypted, zeros)
return encrypted
}
}
38 changes: 38 additions & 0 deletions aes/aes_key_factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
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 (
stdaes "crypto/aes"
"encoding/hex"
"errors"
)

func CreateKeyFromBytes(keyBytes []byte, kcvMethod KCVMethod) (Key, error) {
if len(keyBytes) != 16 && len(keyBytes) != 24 && len(keyBytes) != 32 {
return Key{}, errors.New("AES key must be 16, 24, or 32 bytes")
}

keyBlock, err := stdaes.NewCipher(keyBytes)
if err != nil {
return Key{}, errors.New("invalid AES key")
}
return Key{keyBlock, keyBytes, kcvMethod}, nil
Comment on lines +25 to +29

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

CreateKeyFromBytes stores the caller-provided keyBytes slice directly in the returned Key. Since slices are mutable, external mutation after creation would change the in-memory key unexpectedly. Consider copying keyBytes before storing it, and similarly have GetKeyBytes() return a defensive copy to prevent accidental mutation of sensitive key material.

Suggested change
keyBlock, err := stdaes.NewCipher(keyBytes)
if err != nil {
return Key{}, errors.New("invalid AES key")
}
return Key{keyBlock, keyBytes, kcvMethod}, nil
keyBytesCopy := append([]byte(nil), keyBytes...)
keyBlock, err := stdaes.NewCipher(keyBytesCopy)
if err != nil {
return Key{}, errors.New("invalid AES key")
}
return Key{keyBlock, keyBytesCopy, kcvMethod}, nil

Copilot uses AI. Check for mistakes.
}

func CreateKeyFromString(key string, kcvMethod KCVMethod) (Key, error) {
keyBytes, err := hex.DecodeString(key)
if err != nil {
return Key{}, errors.New("AES key is not in correct hex format")
}
return CreateKeyFromBytes(keyBytes, kcvMethod)
}
80 changes: 80 additions & 0 deletions aes/aes_key_factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
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 TestCreateKeyFromStringInvalidHex(t *testing.T) {
_, err := CreateKeyFromString("not-hex", KCVMethodECB)
if err == nil {
t.Fatal("should fail with invalid hex")
}
}

func TestCreateKeyFromStringWrongLength(t *testing.T) {
_, err := CreateKeyFromString("AABBCCDD", KCVMethodECB)
if err == nil {
t.Fatal("should fail with wrong key length")
}
}

func TestCreateKeyFromBytesWrongLength(t *testing.T) {
_, err := CreateKeyFromBytes([]byte{1, 2, 3}, KCVMethodECB)
if err == nil {
t.Fatal("should fail with wrong key length")
}
}

func TestCreateKeyFromBytes16(t *testing.T) {
key, err := CreateKeyFromBytes(make([]byte, 16), KCVMethodECB)
if err != nil {
t.Fatalf("failed to create AES-128 key: %v", err)
}
if len(key.KeyBytes) != 16 {
t.Fatalf("expected 16 bytes but got %d", len(key.KeyBytes))
}
}

func TestCreateKeyFromBytes24(t *testing.T) {
key, err := CreateKeyFromBytes(make([]byte, 24), KCVMethodECB)
if err != nil {
t.Fatalf("failed to create AES-192 key: %v", err)
}
if len(key.KeyBytes) != 24 {
t.Fatalf("expected 24 bytes but got %d", len(key.KeyBytes))
}
}

func TestCreateKeyFromBytes32(t *testing.T) {
key, err := CreateKeyFromBytes(make([]byte, 32), KCVMethodECB)
if err != nil {
t.Fatalf("failed to create AES-256 key: %v", err)
}
if len(key.KeyBytes) != 32 {
t.Fatalf("expected 32 bytes but got %d", len(key.KeyBytes))
}
}

func TestCreateKeyFromStringValid(t *testing.T) {
key, err := CreateKeyFromString("702E73B9230ECADBB8F120BDE3870493", KCVMethodCMAC)
if err != nil {
t.Fatalf("failed to create key from hex string: %v", err)
}
if len(key.KeyBytes) != 16 {
t.Fatalf("expected 16 bytes but got %d", len(key.KeyBytes))
}
if key.kcvMethod != KCVMethodCMAC {
t.Fatal("expected CMAC KCV method")
}
}
104 changes: 104 additions & 0 deletions aes/aes_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
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 (
"strings"
"testing"
)

const testKeyHex = "702E73B9230ECADBB8F120BDE3870493"

func TestECBCheckValue(t *testing.T) {
key, err := CreateKeyFromString(testKeyHex, KCVMethodECB)
if err != nil {
t.Fatalf("failed to create key: %v", err)
}

expected := "A3DB34"
if !strings.EqualFold(key.CheckValue(), expected) {
t.Fatalf("expected ECB KCV %s but got %s", expected, key.CheckValue())
}
}

func TestECBVerifyCheckValue(t *testing.T) {
key, err := CreateKeyFromString(testKeyHex, KCVMethodECB)
if err != nil {
t.Fatalf("failed to create key: %v", err)
}

if !key.VerifyCheckValue("A3DB34") {
t.Fatal("ECB KCV verification should pass")
}
if key.VerifyCheckValue("000000") {
t.Fatal("ECB KCV verification should fail with wrong value")
}
}

func TestCMACCheckValue(t *testing.T) {
key, err := CreateKeyFromString(testKeyHex, KCVMethodCMAC)
if err != nil {
t.Fatalf("failed to create key: %v", err)
}

expected := "107f4d"
if !strings.EqualFold(key.CheckValue(), expected) {
t.Fatalf("expected CMAC KCV %s but got %s", expected, key.CheckValue())
}
}

func TestCMACVerifyCheckValue(t *testing.T) {
key, err := CreateKeyFromString(testKeyHex, KCVMethodCMAC)
if err != nil {
t.Fatalf("failed to create key: %v", err)
}

if !key.VerifyCheckValue("107F4D") {
t.Fatal("CMAC KCV verification should pass")
}
if key.VerifyCheckValue("A3DB34") {
t.Fatal("CMAC KCV verification should fail with ECB value")
}
}

func TestVerifyCheckValueCaseInsensitive(t *testing.T) {
key, err := CreateKeyFromString(testKeyHex, KCVMethodECB)
if err != nil {
t.Fatalf("failed to create key: %v", err)
}

if !key.VerifyCheckValue("a3db34") {
t.Fatal("KCV verification should be case insensitive")
}
}

func TestVerifyCheckValueTooShort(t *testing.T) {
key, err := CreateKeyFromString(testKeyHex, KCVMethodECB)
if err != nil {
t.Fatalf("failed to create key: %v", err)
}

if key.VerifyCheckValue("A3") {
t.Fatal("KCV with less than 2 bytes should fail")
}
}

func TestGetKeyBytes(t *testing.T) {
key, err := CreateKeyFromBytes(mustDecodeHex(testKeyHex), KCVMethodECB)
if err != nil {
t.Fatalf("failed to create key: %v", err)
}

if len(key.GetKeyBytes()) != 16 {
t.Fatalf("expected 16 bytes but got %d", len(key.GetKeyBytes()))
}
}
74 changes: 74 additions & 0 deletions aes/cmac.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
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/cipher"

// rb is the constant Rb for 128-bit block ciphers as defined in RFC 4493.
var rb = byte(0x87)

// cmac computes AES-CMAC per RFC 4493 for a message whose length is a
// multiple of the block size (16 bytes). For KCV computation the message
// is always 16 zero bytes, so partial-block padding is not needed.
func cmac(block cipher.Block, message []byte) []byte {
k1 := generateSubkey(block)

n := len(message) / block.BlockSize()
if n == 0 {
n = 1
}
lastBlock := message[(n-1)*block.BlockSize():]

xored := make([]byte, block.BlockSize())
for i := range xored {
xored[i] = lastBlock[i] ^ k1[i]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle empty CMAC input without panicking

The CMAC helper claims to support messages whose length is a multiple of the block size, but len(message)==0 (a valid multiple and valid RFC 4493 case) causes lastBlock to be empty and then indexed in the xored loop, which panics. Any reuse of this helper with an empty message would crash the process instead of returning a deterministic MAC or a controlled error.

Useful? React with 👍 / 👎.

}

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

cmac() will panic for len(message) == 0 (and also for any len(message) not an exact multiple of the block size), because lastBlock becomes shorter than block.BlockSize() and is indexed at lastBlock[i]. Even if current callers only use 16 zero bytes, this is a sharp edge for future reuse. Prefer to (a) validate len(message) > 0 and len(message)%block.BlockSize()==0 and fail deterministically, or (b) implement full RFC 4493 CMAC including padding/K2 and return an error on invalid inputs (changing the signature to return ([]byte, error) if needed).”

Copilot uses AI. Check for mistakes.

x := make([]byte, block.BlockSize())
for i := 0; i < n-1; i++ {
start := i * block.BlockSize()
for j := 0; j < block.BlockSize(); j++ {
x[j] ^= message[start+j]
}
block.Encrypt(x, x)
}

for j := 0; j < block.BlockSize(); j++ {
x[j] ^= xored[j]
}
block.Encrypt(x, x)

return x
}

// generateSubkey derives the CMAC subkey K1 from the cipher block per RFC 4493.
func generateSubkey(block cipher.Block) []byte {
l := make([]byte, block.BlockSize())
block.Encrypt(l, l)

k1 := leftShift(l)
if l[0]&0x80 != 0 {
k1[len(k1)-1] ^= rb
}

return k1
}

// leftShift performs a one-bit left shift on a byte slice.
func leftShift(input []byte) []byte {
output := make([]byte, len(input))
for i := 0; i < len(input)-1; i++ {
output[i] = (input[i] << 1) | (input[i+1] >> 7)
}
output[len(input)-1] = input[len(input)-1] << 1
return output
}
Loading
Loading