penguin/homekey-go

homekey-go

internal/app/reader.go

raw ยท 4248 bytes

package app

import (
	"bytes"
	"context"
	"fmt"
	"homekey.local/provisioner/internal/blegateway"
	"homekey.local/provisioner/internal/nfcauth"
	"homekey.local/provisioner/internal/provision"
	"log"
	"time"
)

func Assigned(locks []*Lock, id string) []*Lock {
	var out []*Lock
	for _, l := range locks {
		for _, r := range l.Config.Readers {
			if r == id {
				out = append(out, l)
				break
			}
		}
	}
	return out
}
func Group(locks []*Lock) ([]byte, error) {
	var group []byte
	for _, l := range locks {
		g := l.Store.ReaderGroupIdentifier()
		if len(g) == 0 {
			continue
		}
		if group != nil && !bytes.Equal(group, g) {
			return nil, fmt.Errorf("shared reader locks belong to different Home Key groups; use separate readers")
		}
		group = g
	}
	return group, nil
}
func Authenticate(ctx, serviceCtx context.Context, locks []*Lock, reader string, card blegateway.Card, relay *blegateway.Relay) (err error) {
	if card.SAK&0x20 == 0 {
		return fmt.Errorf("card does not support ISO-DEP")
	}
	success := false
	defer func() {
		finishCtx, done := context.WithTimeout(serviceCtx, 2*time.Second)
		defer done()
		if e := nfcauth.Finish(finishCtx, relay, success); e != nil {
			log.Printf("NFC control flow: %v", e)
		}
	}()
	if _, err = Group(locks); err != nil {
		return err
	}
	// All locks in one Apple Home share the reader group key. Use one enrolled
	// identity for this transaction; recheck each lock's own endpoint enrollment.
	var source *Lock
	for _, l := range locks {
		if s := l.Store.Summary(); s.ReaderProvisioned && s.Endpoints > 0 {
			source = l
			break
		}
	}
	if source == nil {
		return fmt.Errorf("no assigned lock has enrolled Home Key credentials")
	}
	snapshots := make(map[*Lock]provision.Credentials)
	credentials := source.Store.AuthenticationCredentials()
	credentials.Issuers = make(map[string]provision.Issuer)
	for _, l := range locks {
		snapshot := l.Store.AuthenticationCredentials()
		snapshots[l] = snapshot
		if snapshot.ReaderPrivateKey != credentials.ReaderPrivateKey {
			continue
		}
		for iid, issuer := range snapshot.Issuers {
			merged, ok := credentials.Issuers[iid]
			if !ok {
				merged = provision.Issuer{PublicKey: issuer.PublicKey, Endpoints: map[string]provision.Endpoint{}}
			}
			for eid, endpoint := range issuer.Endpoints {
				if endpoint.KeyState != 1 {
					continue
				}
				if existing, ok := merged.Endpoints[eid]; ok && existing.PublicKey != endpoint.PublicKey {
					return fmt.Errorf("conflicting endpoint enrollment")
				}
				merged.Endpoints[eid] = endpoint
			}
			credentials.Issuers[iid] = merged
		}
	}
	result, err := nfcauth.Authenticate(ctx, relay, credentials)
	if err != nil {
		return err
	}
	// Persistent keys depend on the identity used in AUTH0. Only cache on
	// that identity's lock, and only if it enrolled the authenticated endpoint.
	sourceSnapshot := snapshots[source]
	if issuer, ok := sourceSnapshot.Issuers[result.IssuerID]; ok {
		if ep, ok := issuer.Endpoints[result.EndpointID]; ok && ep.KeyState == 1 {
			if err = source.Store.SaveAuthenticatedKey(sourceSnapshot, result.IssuerID, result.EndpointID, result.PublicKey, result.PersistentKey); err != nil {
				return err
			}
		}
	}
	success = unlockAuthorized(ctx, locks, snapshots, credentials.ReaderPrivateKey, result, reader, card.Session) > 0
	if !success {
		return fmt.Errorf("no assigned lock authorizes this endpoint")
	}
	return nil
}

func unlockAuthorized(ctx context.Context, locks []*Lock, snapshots map[*Lock]provision.Credentials, groupKey string, result *nfcauth.Result, reader string, session uint32) int {
	unlocked := 0
	for _, l := range locks {
		expected := snapshots[l]
		if expected.ReaderPrivateKey != groupKey {
			continue
		}
		e := l.Store.WithAuthorization(expected, result.IssuerID, result.EndpointID, result.PublicKey, func() error {
			if err := ctx.Err(); err != nil {
				return err
			}
			if err := l.Device.Lock.LockTargetState.SetValue(0); err != nil {
				return err
			}
			return l.Device.Lock.LockCurrentState.SetValue(0)
		})
		if e != nil {
			continue
		}
		unlocked++
		log.Printf("Home Key authenticated reader=%s session=%d endpoint=%s lock=%s; virtual lock unlocked", reader, session, result.EndpointID, l.Config.ID)
	}
	return unlocked
}