Initial commit
This commit is contained in:
280
internal/speedport/ccm.go
Normal file
280
internal/speedport/ccm.go
Normal file
@@ -0,0 +1,280 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user