penguin/homekey-go

homekey-go

internal/provision/control.go

raw ยท 5037 bytes

// SPDX-License-Identifier: Apache-2.0
// Adapted from kormax/apple-home-key-reader entity.py/service.py; see NOTICE.
package provision

import (
	"crypto/elliptic"
	"crypto/sha1"
	"encoding/hex"
	"fmt"
	"log"
	"math/big"
	"time"
)

const (
	Success byte = iota
	OutOfResources
	Duplicate
	DoesNotExist
	NotSupported
)
const Capacity = 16

func validScalar(b []byte) bool {
	n := new(big.Int).SetBytes(b)
	return len(b) == 32 && n.Sign() > 0 && n.Cmp(elliptic.P256().Params().N) < 0
}
func endpointID(pub []byte) string                  { h := sha1.Sum(pub); return hex.EncodeToString(h[:6]) }
func statusResponse(outer, tag, status byte) []byte { return TLV(outer, TLV(tag, []byte{status})) }

// Handle validates and commits a control point operation before returning its
// response. Malformed input is a HAP error, semantic failures are TLV statuses.
func (s *Store) Handle(raw []byte) (response []byte, err error) {
	f, err := DecodeTLV(raw)
	if err != nil {
		return nil, err
	}
	op, err := one(f, 1)
	if err != nil {
		return nil, err
	}
	r, hasReader := f[6]
	e, hasEndpoint := f[4]
	if hasReader == hasEndpoint {
		return nil, fmt.Errorf("exactly one request object required")
	}
	body := r
	if hasEndpoint {
		body = e
	}
	inner, err := DecodeTLV(body)
	if err != nil {
		return nil, err
	}
	log.Printf("Provisioning request: op=%d reader=%t endpoint=%t",
		op, hasReader, hasEndpoint)
	err = s.update(func(d *Data) error {
		if hasReader {
			response, err = readerOperation(&d.HomeKey, op, inner)
		} else {
			response, err = endpointOperation(&d.HomeKey, op, inner)
		}
		return err
	})
	if err != nil {
		return nil, err
	}
	return response, nil
}

func readerOperation(c *Credentials, op byte, f Fields) ([]byte, error) {
	status := func(v byte) ([]byte, error) { return statusResponse(7, 2, v), nil }
	switch op {
	case 1:
		// Match the working Python implementation before enrollment: hash a zero
		// scalar when no reader key exists. This is an identifier, not a usable key.
		key := make([]byte, 32)
		if c.ReaderPrivateKey != "" {
			key, _ = hex.DecodeString(c.ReaderPrivateKey)
		}
		id, _ := hex.DecodeString(keyID(key))
		return TLV(7, TLV(1, id)), nil
	case 2:
		kt, err := one(f, 1)
		if err != nil {
			return nil, err
		}
		if kt != 2 {
			return status(NotSupported)
		}
		if !validScalar(f[2]) || len(f[3]) != 8 {
			return nil, fmt.Errorf("invalid reader key or identifier")
		}
		key, id := hex.EncodeToString(f[2]), hex.EncodeToString(f[3])
		if key == c.ReaderPrivateKey && id == c.ReaderIdentifier {
			return status(Duplicate)
		}
		// A new reader identity invalidates previous FAST secrets.
		for iid, issuer := range c.Issuers {
			for eid, ep := range issuer.Endpoints {
				ep.PersistentKey = ""
				issuer.Endpoints[eid] = ep
			}
			c.Issuers[iid] = issuer
		}
		c.ReaderPrivateKey = key
		c.ReaderIdentifier = id
		return status(Success)
	case 3:
		if len(f[4]) != 8 {
			return nil, fmt.Errorf("invalid reader key identifier")
		}
		if c.ReaderPrivateKey == "" {
			return status(DoesNotExist)
		}
		key, _ := hex.DecodeString(c.ReaderPrivateKey)
		if hex.EncodeToString(f[4]) != keyID(key) {
			return status(DoesNotExist)
		}
		c.ReaderPrivateKey = ""
		c.ReaderIdentifier = ""
		for id, issuer := range c.Issuers {
			issuer.Endpoints = map[string]Endpoint{}
			c.Issuers[id] = issuer
		}
		return status(Success)
	default:
		return status(NotSupported)
	}
}

func endpointOperation(c *Credentials, op byte, f Fields) ([]byte, error) {
	status := func(v byte) ([]byte, error) { return statusResponse(5, 3, v), nil }
	if op != 2 {
		return status(NotSupported)
	} // GET/REMOVE wire semantics not established in reference.
	kt, err := one(f, 1)
	if err != nil {
		return nil, err
	}
	if kt != 2 {
		return status(NotSupported)
	}
	if len(f[2]) != 64 || len(f[3]) != 8 {
		return nil, fmt.Errorf("invalid device credential length")
	}
	state := byte(1)
	if _, ok := f[4]; ok {
		state, err = one(f, 4)
		if err != nil || state > 1 {
			return nil, fmt.Errorf("invalid credential state")
		}
	}
	pub := append([]byte{4}, f[2]...)
	x, _ := elliptic.Unmarshal(elliptic.P256(), pub)
	if x == nil {
		return nil, fmt.Errorf("invalid P-256 public key")
	}
	iid := hex.EncodeToString(f[3])
	issuer, ok := c.Issuers[iid]
	if !ok {
		return status(DoesNotExist)
	}
	if c.ReaderPrivateKey == "" {
		return status(DoesNotExist)
	}
	eid := endpointID(pub)
	old, exists := issuer.Endpoints[eid]
	if exists && old.PublicKey != hex.EncodeToString(pub) {
		return status(NotSupported)
	}
	code := Success
	if exists && old.KeyState == state {
		code = Duplicate
	} else {
		total := 0
		for _, i := range c.Issuers {
			total += len(i.Endpoints)
		}
		if !exists && total >= Capacity {
			return status(OutOfResources)
		}
		if !exists {
			old = Endpoint{PublicKey: hex.EncodeToString(pub), KeyType: 2, EnrolledAt: time.Now().Unix()}
		}
		old.KeyState = state
		issuer.Endpoints[eid] = old
		c.Issuers[iid] = issuer
	}
	body := append(TLV(2, f[3]), TLV(3, []byte{code})...)
	return TLV(5, body), nil
}