penguin/homekey-go

homekey-go

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
}