From 1fbc4d520ab2e017cea1e289bd296b29e3a8bec4 Mon Sep 17 00:00:00 2001 From: Darell Tan Date: Sat, 12 Aug 2023 01:35:07 +0800 Subject: [PATCH] Use a more secure PIN config by default The server used to use the hap default PIN, but using a fixed PIN is not secure. A random PIN is now generated on first run and displayed to the console (or journal), similar to how homebridge does it. It can also be specified explicitly by the user in the config file. --- README.md | 4 ++++ bridge.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/hapz2m/main.go | 9 +++++++ 3 files changed, 73 insertions(+) diff --git a/README.md b/README.md index a5dc179..83874cc 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ Additionally, these options control networking aspects for the bridge: These settings are optional and can be left blank. +The pairing code for the server is generated dynamically on first startup +and printed to the console (or systemd journal). Alternatively, you can specify +it using `Pin` in the config file to have it fixed. + License ======== diff --git a/bridge.go b/bridge.go index 31baf06..424aaf9 100644 --- a/bridge.go +++ b/bridge.go @@ -12,9 +12,11 @@ import ( "net/url" "context" + "crypto/rand" "encoding/json" "fmt" "log" + "math/big" "net" "net/http" "reflect" @@ -36,6 +38,9 @@ const ( // Store name for persisting zigbee2mqtt state Z2M_STATE_STORE = "z2m_state" + // Store name for server PIN code + Z2M_PIN_STORE = "z2m_pin" + // timeout for UpdateZ2MState Z2M_UPDATE_TIMEOUT = 3 * time.Second @@ -65,6 +70,7 @@ type Bridge struct { devices map[string]*BridgeDevice server *hap.Server store hap.Store + pin string // RWMutex protects hap init variables below hapInitMutex sync.RWMutex @@ -102,6 +108,51 @@ func NewBridge(ctx context.Context, storeDir string) *Bridge { return br } +// Sets the PIN code for the HAP server. +// If the given pin is empty, it will be read from the store, or failing that, +// one will be generated +func (br *Bridge) SetPin(pin string) (string, error) { + // if PIN was not explicitly specified, we re-use the existing one from store + if pin == "" { + if storePin, err := br.store.Get(Z2M_PIN_STORE); err == nil { + pin = string(storePin) + } + } + + savePin := pin == "" + + if pin == "" { + for { + rnd, err := rand.Int(rand.Reader, big.NewInt(99999999+1)) + if err != nil { + return "", fmt.Errorf("can't generate PIN: %v", err) + } + + // pad if necessary + pin = rnd.Text(10) + "00000000" + pin = pin[:8] + + // ensure it's not an insecure PIN + if !hap.InvalidPins[pin] { + break + } + } + } else if hap.InvalidPins[pin] { + return "", fmt.Errorf("insecure pin %s", pin) + } + + // persist the PIN + if savePin { + br.store.Set(Z2M_PIN_STORE, []byte(pin)) + } + + br.pin = pin + return pin, nil +} + +// Returns the PIN +func (br *Bridge) GetPin() string { return br.pin } + // Initializes the hap.Server and calls ListenAndServe(). // ListenAndServe() will block until the context is cancelled func (br *Bridge) StartHAP() error { @@ -109,6 +160,13 @@ func (br *Bridge) StartHAP() error { return fmt.Errorf("bridge accessory not created yet") } + // initialize PIN, either from store or dynamically generated + if br.pin == "" { + if _, err := br.SetPin(""); err != nil { + return err + } + } + br.hapInitMutex.RLock() if !br.hapInitDone { br.hapInitMutex.RUnlock() @@ -124,6 +182,8 @@ func (br *Bridge) StartHAP() error { return err } + br.server.Pin = br.pin + br.server.Addr = br.ListenAddr br.server.Ifaces = br.Interfaces diff --git a/cmd/hapz2m/main.go b/cmd/hapz2m/main.go index 971f37c..0b39e2b 100644 --- a/cmd/hapz2m/main.go +++ b/cmd/hapz2m/main.go @@ -36,6 +36,8 @@ type config struct { ListenAddr string Interfaces []string + Pin string + Server, Username, Password string } @@ -89,6 +91,10 @@ func main() { log.Fatalf("-quiet and -debug options are mutually-exclusive") } + if _, err := br.SetPin(cfg.Pin); err != nil { + log.Fatalf("cannot set PIN code: %v", err) + } + // validate ListenAddr if specified if cfg.ListenAddr != "" { _, _, err := net.SplitHostPort(cfg.ListenAddr) @@ -120,6 +126,9 @@ func main() { log.Println("hapz2m configured. starting HAP server...") + pin := br.GetPin() + log.Printf("server PIN is %s-%s", pin[:4], pin[4:]) + err = br.StartHAP() if err != nil { if err == http.ErrServerClosed {