internal/hkserver/server.go
raw ยท 3897 bytes
// SPDX-License-Identifier: Apache-2.0
package hkserver
import (
"encoding/base64"
"encoding/hex"
"fmt"
"log"
"net/http"
"github.com/brutella/hap"
"github.com/brutella/hap/accessory"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/service"
"homekey.local/provisioner/internal/provision"
)
type Config struct {
Name, Serial, Finish, PIN, Address string
Interfaces []string
}
type Device struct {
Server *hap.Server
Accessory *accessory.A
Control *characteristic.C
Lock *service.LockMechanism
}
func bytesCharacteristic(t string, raw []byte, permissions ...string) *characteristic.C {
c := characteristic.NewBytes(t)
c.Permissions = permissions
c.SetValue(raw)
return c.C
}
func New(store *provision.Store, cfg Config) (*Device, error) {
colors := map[string]string{"black": "00000000", "tan": "ced5da00", "gold": "aad6ec00", "silver": "e3e3e300"}
color, ok := colors[cfg.Finish]
if !ok {
return nil, fmt.Errorf("finish must be black, tan, gold, or silver")
}
finish, _ := hex.DecodeString(color)
// HAP category 6 is a standalone door lock, not a bridge.
a := accessory.New(accessory.Info{Name: cfg.Name, SerialNumber: cfg.Serial, Manufacturer: "DIY", Model: "Go Home Key", Firmware: "0.1.0"}, 6)
a.IdentifyFunc = func(*http.Request) { log.Print("Identify: virtual Home Key lock") }
a.Info.AddC(bytesCharacteristic("26C", provision.TLV(1, finish), characteristic.PermissionRead))
lock := service.NewLockMechanism()
lock.Primary = true
lock.LockCurrentState.SetValue(1)
lock.LockTargetState.SetValue(1)
lock.LockTargetState.ValidVals = []int{0, 1}
lock.LockTargetState.OnSetRemoteValue(func(v int) error {
log.Printf("Virtual lock state: %d (simulation; no physical lock connected)", v)
return lock.LockCurrentState.SetValue(v)
})
a.AddS(lock.S)
management := service.NewLockManagement()
management.Version.SetValue("1.0")
management.LockControlPoint.SetValue([]byte{})
management.LockControlPoint.Stateless = true
management.LockControlPoint.SetValueRequestFunc = func(any, *http.Request) (any, int) { return nil, -70406 }
a.AddS(management.S)
nfc := service.New("266")
state := characteristic.NewInt("263")
state.Format = characteristic.FormatUInt16
state.Permissions = []string{characteristic.PermissionRead, characteristic.PermissionEvents}
state.SetValue(0) // Matches the reference implementation; semantics not established.
nfc.AddC(state.C)
control := bytesCharacteristic("264", []byte{}, characteristic.PermissionRead, characteristic.PermissionWrite, characteristic.PermissionWriteResponse)
control.Stateless = true
control.ValueRequestFunc = func(*http.Request) (any, int) { return "", 0 }
control.SetValueRequestFunc = func(value any, _ *http.Request) (any, int) {
encoded, ok := value.(string)
if !ok || len(encoded) > base64.StdEncoding.EncodedLen(provision.MaxMessage) {
return nil, -70410
}
raw, err := base64.StdEncoding.Strict().DecodeString(encoded)
if err != nil {
return nil, -70410
}
response, err := store.Handle(raw)
if err != nil {
// No raw TLVs, keys, public keys or credential payloads in logs.
log.Printf("Provisioning request rejected: %v", err)
return nil, -70402
}
v := store.Summary()
log.Printf("Provisioning exchange complete: reader=%t issuers=%d endpoints=%d", v.ReaderProvisioned, v.Issuers, v.Endpoints)
return base64.StdEncoding.EncodeToString(response), 0
}
nfc.AddC(control)
supported := append(provision.TLV(1, []byte{provision.Capacity}), provision.TLV(2, []byte{provision.Capacity})...)
nfc.AddC(bytesCharacteristic("265", supported, characteristic.PermissionRead))
a.AddS(nfc)
s, err := hap.NewServer(store, a)
if err != nil {
return nil, err
}
s.Pin = cfg.PIN
s.Addr = cfg.Address
s.Ifaces = cfg.Interfaces
return &Device{Server: s, Accessory: a, Control: control, Lock: lock}, nil
}