drivers/sesame/crypto.go
raw ยท 4166 bytes
// SPDX-License-Identifier: Apache-2.0
package sesame
import (
"crypto/aes"
"crypto/cipher"
"crypto/subtle"
"encoding/binary"
"errors"
"math"
)
// RFC 4493 AES-CMAC (full 128-bit output).
func cmac(key, data []byte) ([]byte, error) {
b, e := aes.NewCipher(key)
if e != nil {
return nil, e
}
double := func(x []byte) []byte {
r := make([]byte, 16)
var c byte
for i := 15; i >= 0; i-- {
r[i] = x[i]<<1 | c
c = x[i] >> 7
}
if c != 0 {
r[15] ^= 0x87
}
return r
}
l := make([]byte, 16)
b.Encrypt(l, l)
k1 := double(l)
k2 := double(k1)
n := (len(data) + 15) / 16
if n == 0 {
n = 1
}
last := make([]byte, 16)
copy(last, data[(n-1)*16:])
k := k1
if len(data) == 0 || len(data)%16 != 0 {
last[len(data)%16] = 0x80
k = k2
}
state := make([]byte, 16)
for i := 0; i < n-1; i++ {
for j := range state {
state[j] ^= data[i*16+j]
}
b.Encrypt(state, state)
}
for j := range state {
state[j] ^= last[j] ^ k[j]
}
b.Encrypt(state, state)
return state, nil
}
// Fixed OS3 CCM profile, NIST SP 800-38C: nonce 13, tag 4, L=2, AAD 00.
// It deliberately supports only this protocol profile, not a general AEAD API.
func ccmTag(b cipher.Block, nonce, plain []byte) []byte {
state := make([]byte, 16)
state[0] = 0x49
copy(state[1:], nonce)
binary.BigEndian.PutUint16(state[14:], uint16(len(plain)))
b.Encrypt(state, state)
// Encoded AAD: two-byte length 1, byte 00, then zero padding.
state[1] ^= 1
b.Encrypt(state, state)
for off := 0; off < len(plain); off += 16 {
n := len(plain) - off
if n > 16 {
n = 16
}
for i := 0; i < n; i++ {
state[i] ^= plain[off+i]
}
b.Encrypt(state, state)
}
return state[:4]
}
func ccmStream(b cipher.Block, nonce []byte, count uint16) []byte {
in := make([]byte, 16)
in[0] = 1
copy(in[1:], nonce)
binary.BigEndian.PutUint16(in[14:], count)
b.Encrypt(in, in)
return in
}
func ccmSeal(b cipher.Block, nonce, plain []byte) ([]byte, error) {
if len(nonce) != 13 || len(plain) > maxMessage-4 {
return nil, errors.New("invalid CCM input")
}
out := make([]byte, len(plain)+4)
tag := ccmTag(b, nonce, plain)
s0 := ccmStream(b, nonce, 0)
for off := 0; off < len(plain); off += 16 {
s := ccmStream(b, nonce, uint16(off/16+1))
for j := 0; j < 16 && off+j < len(plain); j++ {
out[off+j] = plain[off+j] ^ s[j]
}
}
for i := 0; i < 4; i++ {
out[len(plain)+i] = tag[i] ^ s0[i]
}
return out, nil
}
func ccmOpen(b cipher.Block, nonce, in []byte) ([]byte, error) {
if len(nonce) != 13 || len(in) < 4 || len(in) > maxMessage {
return nil, errors.New("invalid CCM input")
}
n := len(in) - 4
plain := make([]byte, n)
for off := 0; off < n; off += 16 {
s := ccmStream(b, nonce, uint16(off/16+1))
for j := 0; j < 16 && off+j < n; j++ {
plain[off+j] = in[off+j] ^ s[j]
}
}
tag := ccmTag(b, nonce, plain)
s0 := ccmStream(b, nonce, 0)
for i := 0; i < 4; i++ {
tag[i] ^= s0[i]
}
if subtle.ConstantTimeCompare(tag, in[n:]) != 1 {
return nil, errors.New("Sesame authentication tag failed")
}
return plain, nil
}
type sessionCipher struct {
block cipher.Block
token [4]byte
tx, rx uint64
}
func newCipher(secret, token []byte) (*sessionCipher, []byte, error) {
if len(secret) != 16 || len(token) != 4 {
return nil, nil, errors.New("unsupported Sesame key/token length")
}
auth, e := cmac(secret, token)
if e != nil {
return nil, nil, e
}
b, e := aes.NewCipher(auth)
if e != nil {
return nil, nil, e
}
s := &sessionCipher{block: b}
copy(s.token[:], token)
return s, auth, nil
}
func (s *sessionCipher) nonce(counter uint64) []byte {
n := make([]byte, 13)
binary.LittleEndian.PutUint64(n, counter)
copy(n[9:], s.token[:])
return n
}
func (s *sessionCipher) encrypt(b []byte) ([]byte, error) {
if s.tx == math.MaxUint64 {
return nil, errors.New("Sesame counter exhausted")
}
out, e := ccmSeal(s.block, s.nonce(s.tx), b)
if e == nil {
s.tx++
}
return out, e
}
func (s *sessionCipher) decrypt(b []byte) ([]byte, error) {
if s.rx == math.MaxUint64 {
return nil, errors.New("Sesame counter exhausted")
}
out, e := ccmOpen(s.block, s.nonce(s.rx), b)
if e == nil {
s.rx++
}
return out, e
}