Files
speedport-exporter/internal/speedport/ccm.go
2026-03-07 16:31:30 +01:00

281 lines
6.8 KiB
Go

package speedport
import (
"crypto/aes"
"crypto/subtle"
"encoding/binary"
"errors"
"fmt"
)
// CCM implements AES-CCM (Counter with CBC-MAC) as defined in RFC 3610.
// This is needed because Go's standard library does not include CCM.
type CCM struct {
key []byte
tagSize int
}
// NewCCM creates a new AES-CCM instance.
// tagSize is the authentication tag length in bytes (4, 6, 8, 10, 12, 14, or 16).
func NewCCM(key []byte, tagSize int) (*CCM, error) {
if tagSize < 4 || tagSize > 16 || tagSize%2 != 0 {
return nil, fmt.Errorf("invalid tag size %d: must be even and between 4-16", tagSize)
}
switch len(key) {
case 16, 24, 32:
// valid AES key sizes
default:
return nil, fmt.Errorf("invalid key size %d", len(key))
}
return &CCM{key: key, tagSize: tagSize}, nil
}
// Decrypt decrypts and authenticates ciphertextWithTag using the given nonce and optional additional data.
// ciphertextWithTag = ciphertext || tag (tag is appended at the end).
func (c *CCM) Decrypt(nonce, ciphertextWithTag, additionalData []byte) ([]byte, error) {
if len(nonce) < 7 || len(nonce) > 13 {
return nil, fmt.Errorf("invalid nonce size %d: must be 7-13 bytes", len(nonce))
}
if len(ciphertextWithTag) < c.tagSize {
return nil, errors.New("ciphertext too short for tag")
}
block, err := aes.NewCipher(c.key)
if err != nil {
return nil, fmt.Errorf("create cipher: %w", err)
}
// Split ciphertext and tag
ct := ciphertextWithTag[:len(ciphertextWithTag)-c.tagSize]
tag := ciphertextWithTag[len(ciphertextWithTag)-c.tagSize:]
// L = 15 - len(nonce) (number of bytes for the length field)
L := 15 - len(nonce)
// --- CTR Decryption ---
// Build A_0 (first counter block, used for tag encryption)
a0 := make([]byte, 16)
a0[0] = byte(L - 1) // flags for counter: just L-1 in lower 3 bits
copy(a0[1:1+len(nonce)], nonce)
// Counter bytes are 0 for A_0
// Encrypt A_0 to get S_0
s0 := make([]byte, 16)
block.Encrypt(s0, a0)
// Decrypt ciphertext using CTR starting from A_1
plaintext := make([]byte, len(ct))
ctr := make([]byte, 16)
copy(ctr, a0)
for i := 0; i < len(ct); i += aes.BlockSize {
// Increment counter (last L bytes, big-endian)
c.incrementCounter(ctr, L)
var keystreamBlock [aes.BlockSize]byte
block.Encrypt(keystreamBlock[:], ctr)
end := i + aes.BlockSize
if end > len(ct) {
end = len(ct)
}
for j := i; j < end; j++ {
plaintext[j] = ct[j] ^ keystreamBlock[j-i]
}
}
// --- CBC-MAC Computation ---
// Build B_0
b0 := make([]byte, 16)
hasAAD := len(additionalData) > 0
aadFlag := byte(0)
if hasAAD {
aadFlag = 1 << 6
}
tagEncoding := byte((c.tagSize-2)/2) << 3
b0[0] = aadFlag | tagEncoding | byte(L-1)
copy(b0[1:1+len(nonce)], nonce)
// Encode message length (plaintext length) in last L bytes, big-endian
msgLen := len(ct)
for i := 0; i < L; i++ {
b0[15-i] = byte(msgLen >> (8 * uint(i)))
}
// Start CBC-MAC: X_1 = E(B_0)
mac := make([]byte, 16)
block.Encrypt(mac, b0)
// Process AAD if present
if hasAAD {
aadBlock := c.formatAAD(additionalData)
for i := 0; i < len(aadBlock); i += aes.BlockSize {
for j := 0; j < aes.BlockSize; j++ {
mac[j] ^= aadBlock[i+j]
}
block.Encrypt(mac, mac)
}
}
// Process plaintext blocks
for i := 0; i < len(plaintext); i += aes.BlockSize {
end := i + aes.BlockSize
if end > len(plaintext) {
// Last partial block: XOR available bytes, rest stays as-is
for j := i; j < len(plaintext); j++ {
mac[j-i] ^= plaintext[j]
}
} else {
for j := 0; j < aes.BlockSize; j++ {
mac[j] ^= plaintext[i+j]
}
}
block.Encrypt(mac, mac)
}
// Compute expected tag: T = first tagSize bytes of (MAC XOR S_0)
expectedTag := make([]byte, c.tagSize)
for i := 0; i < c.tagSize; i++ {
expectedTag[i] = mac[i] ^ s0[i]
}
// Constant-time comparison
if subtle.ConstantTimeCompare(expectedTag, tag) != 1 {
return nil, errors.New("CCM authentication failed: tag mismatch")
}
return plaintext, nil
}
// Encrypt encrypts plaintext using the given nonce and optional additional data.
// Returns ciphertext || tag.
func (c *CCM) Encrypt(nonce, plaintext, additionalData []byte) ([]byte, error) {
if len(nonce) < 7 || len(nonce) > 13 {
return nil, fmt.Errorf("invalid nonce size %d: must be 7-13 bytes", len(nonce))
}
block, err := aes.NewCipher(c.key)
if err != nil {
return nil, fmt.Errorf("create cipher: %w", err)
}
L := 15 - len(nonce)
// --- CBC-MAC Computation ---
b0 := make([]byte, 16)
hasAAD := len(additionalData) > 0
aadFlag := byte(0)
if hasAAD {
aadFlag = 1 << 6
}
tagEncoding := byte((c.tagSize-2)/2) << 3
b0[0] = aadFlag | tagEncoding | byte(L-1)
copy(b0[1:1+len(nonce)], nonce)
msgLen := len(plaintext)
for i := 0; i < L; i++ {
b0[15-i] = byte(msgLen >> (8 * uint(i)))
}
mac := make([]byte, 16)
block.Encrypt(mac, b0)
if hasAAD {
aadBlock := c.formatAAD(additionalData)
for i := 0; i < len(aadBlock); i += aes.BlockSize {
for j := 0; j < aes.BlockSize; j++ {
mac[j] ^= aadBlock[i+j]
}
block.Encrypt(mac, mac)
}
}
for i := 0; i < len(plaintext); i += aes.BlockSize {
end := i + aes.BlockSize
if end > len(plaintext) {
for j := i; j < len(plaintext); j++ {
mac[j-i] ^= plaintext[j]
}
} else {
for j := 0; j < aes.BlockSize; j++ {
mac[j] ^= plaintext[i+j]
}
}
block.Encrypt(mac, mac)
}
// --- CTR Encryption ---
a0 := make([]byte, 16)
a0[0] = byte(L - 1)
copy(a0[1:1+len(nonce)], nonce)
s0 := make([]byte, 16)
block.Encrypt(s0, a0)
ciphertext := make([]byte, len(plaintext))
ctr := make([]byte, 16)
copy(ctr, a0)
for i := 0; i < len(plaintext); i += aes.BlockSize {
c.incrementCounter(ctr, L)
var keystreamBlock [aes.BlockSize]byte
block.Encrypt(keystreamBlock[:], ctr)
end := i + aes.BlockSize
if end > len(plaintext) {
end = len(plaintext)
}
for j := i; j < end; j++ {
ciphertext[j] = plaintext[j] ^ keystreamBlock[j-i]
}
}
// Compute tag
tag := make([]byte, c.tagSize)
for i := 0; i < c.tagSize; i++ {
tag[i] = mac[i] ^ s0[i]
}
return append(ciphertext, tag...), nil
}
// formatAAD formats additional authenticated data per RFC 3610 Section 2.2.
func (c *CCM) formatAAD(aad []byte) []byte {
var encoded []byte
aadLen := len(aad)
if aadLen < 0xFF00 {
encoded = make([]byte, 2)
binary.BigEndian.PutUint16(encoded, uint16(aadLen))
} else if aadLen < (1 << 32) {
encoded = make([]byte, 6)
encoded[0] = 0xFF
encoded[1] = 0xFE
binary.BigEndian.PutUint32(encoded[2:], uint32(aadLen))
}
encoded = append(encoded, aad...)
// Pad to block boundary
if rem := len(encoded) % aes.BlockSize; rem != 0 {
encoded = append(encoded, make([]byte, aes.BlockSize-rem)...)
}
return encoded
}
// incrementCounter increments the last L bytes of the counter block (big-endian).
func (c *CCM) incrementCounter(ctr []byte, L int) {
for i := 15; i >= 16-L; i-- {
ctr[i]++
if ctr[i] != 0 {
return
}
}
}