Files
go2rtc/pkg/hap/client_pairing.go
2023-07-23 22:22:36 +03:00

377 lines
8.6 KiB
Go

package hap
import (
"bufio"
"crypto/sha512"
"errors"
"fmt"
"net"
"strings"
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
"github.com/AlexxIT/go2rtc/pkg/hap/ed25519"
"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
"github.com/AlexxIT/go2rtc/pkg/mdns"
"github.com/tadglines/go-pkgs/crypto/srp"
)
func Pair(deviceID, pin string) (*Client, error) {
var addr string
var mfi bool
_ = mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
if entry.Complete() && entry.Info["id"] == deviceID {
addr = entry.Addr()
mfi = entry.Info["ff"] == "1"
return true
}
return false
})
if addr == "" {
return nil, errors.New("hap: mdns.Discovery")
}
c := &Client{
DeviceAddress: addr,
DeviceID: deviceID,
ClientID: GenerateUUID(),
ClientPrivate: GenerateKey(),
}
return c, c.Pair(mfi, pin)
}
func (c *Client) Pair(mfi bool, pin string) (err error) {
pin = strings.ReplaceAll(pin, "-", "")
if len(pin) != 8 {
return fmt.Errorf("wrong PIN format: %s", pin)
}
pin = pin[:3] + "-" + pin[3:5] + "-" + pin[5:] // 123-45-678
c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout)
if err != nil {
return
}
c.reader = bufio.NewReader(c.conn)
// STEP M1. Send HELLO
plainM1 := struct {
Method byte `tlv8:"0"`
State byte `tlv8:"6"`
}{
Method: MethodPair,
State: StateM1,
}
if mfi {
plainM1.Method = MethodPairMFi // ff=1 => method=1, ff=2 => method=0
}
res, err := c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(plainM1))
if err != nil {
return
}
// STEP M2. Read Device Salt and session PublicKey
var plainM2 struct {
Salt []byte `tlv8:"2"`
SessionKey []byte `tlv8:"3"` // server public key, aka session.B
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}
if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil {
return
}
if plainM2.State != StateM2 {
return NewResponseError(plainM1, plainM2)
}
if plainM2.Error != 0 {
return newPairingError(plainM2.Error)
}
// STEP M3. Generate SRP Session using pin
username := []byte("Pair-Setup")
// Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE)
pake, err := srp.NewSRP(
"rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username),
)
if err != nil {
return
}
pake.SaltLength = 16
// username: "Pair-Setup", password: PIN (with dashes)
session := pake.NewClientSession(username, []byte(pin))
sessionShared, err := session.ComputeKey(plainM2.Salt, plainM2.SessionKey)
if err != nil {
return
}
// STEP M3. Send request
plainM3 := struct {
SessionKey []byte `tlv8:"3"`
Proof []byte `tlv8:"4"`
State byte `tlv8:"6"`
}{
SessionKey: session.GetA(), // client public key, aka session.A
Proof: session.ComputeAuthenticator(),
State: StateM3,
}
if res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(plainM3)); err != nil {
return
}
// STEP M4. Read response
var plainM4 struct {
Proof []byte `tlv8:"4"` // server proof
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}
if err = tlv8.UnmarshalReader(res.Body, &plainM4); err != nil {
return
}
if plainM4.State != StateM4 {
return NewResponseError(plainM3, plainM4)
}
if plainM4.Error != 0 {
return newPairingError(plainM4.Error)
}
// STEP M4. Verify response
if !session.VerifyServerAuthenticator(plainM4.Proof) {
return errors.New("hap: wrong server auth")
}
// STEP M5. Generate signature
localSign, err := hkdf.Sha512(
sessionShared, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info",
)
if err != nil {
return
}
b := Append(localSign, c.ClientID, c.ClientPublic())
signature, err := ed25519.Signature(c.ClientPrivate, b)
if err != nil {
return
}
// STEP M5. Generate payload
plainM5 := struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{
Identifier: c.ClientID,
PublicKey: c.ClientPublic(),
Signature: signature,
}
if b, err = tlv8.Marshal(plainM5); err != nil {
return
}
// STEP M5. Encrypt payload
encryptKey, err := hkdf.Sha512(
sessionShared, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info",
)
if err != nil {
return
}
if b, err = chacha20poly1305.Encrypt(encryptKey, "PS-Msg05", b); err != nil {
return
}
// STEP M5. Send request
cipherM5 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
EncryptedData: b,
State: StateM5,
}
if res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(cipherM5)); err != nil {
return
}
// STEP M6. Read response
cipherM6 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &cipherM6); err != nil {
return
}
if cipherM6.State != StateM6 || cipherM6.Error != 0 {
return NewResponseError(plainM5, cipherM6)
}
// STEP M6. Decrypt payload
b, err = chacha20poly1305.Decrypt(encryptKey, "PS-Msg06", cipherM6.EncryptedData)
if err != nil {
return
}
plainM6 := struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{}
if err = tlv8.Unmarshal(b, &plainM6); err != nil {
return
}
// STEP M6. Verify payload
remoteSign, err := hkdf.Sha512(
sessionShared, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info",
)
if err != nil {
return
}
b = Append(remoteSign, plainM6.Identifier, plainM6.PublicKey)
if !ed25519.ValidateSignature(plainM6.PublicKey, b, plainM6.Signature) {
return errors.New("hap: wrong accessory sign")
}
if c.DeviceID != plainM6.Identifier {
return errors.New("hap: wrong DeviceID: " + plainM6.Identifier)
}
c.DevicePublic = plainM6.PublicKey
return nil
}
func (c *Client) ListPairings() error {
plainM1 := struct {
Method byte `tlv8:"0"`
State byte `tlv8:"6"`
}{
Method: MethodListPairings,
State: StateM1,
}
res, err := c.Post(PathPairings, MimeTLV8, tlv8.MarshalReader(plainM1))
if err != nil {
return err
}
// TODO: don't know how to fix array of items
var plainM2 struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
State byte `tlv8:"6"`
Permission byte `tlv8:"11"`
}
if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil {
return err
}
return nil
}
func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) error {
plainM1 := struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
State byte `tlv8:"6"`
Permission byte `tlv8:"11"`
}{
Method: MethodAddPairing,
Identifier: clientID,
PublicKey: clientPublic,
State: StateM1,
Permission: PermissionUser,
}
if admin {
plainM1.Permission = PermissionAdmin
}
res, err := c.Post(PathPairings, MimeTLV8, tlv8.MarshalReader(plainM1))
if err != nil {
return err
}
var plainM2 struct {
State byte `tlv8:"6"`
Unknown byte `tlv8:"7"`
}
if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil {
return err
}
return nil
}
func (c *Client) DeletePairing(id string) error {
plainM1 := struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
State byte `tlv8:"6"`
}{
Method: MethodDeletePairing,
Identifier: id,
State: StateM1,
}
res, err := c.Post(PathPairings, MimeTLV8, tlv8.MarshalReader(plainM1))
if err != nil {
return err
}
var plainM2 struct {
State byte `tlv8:"6"`
}
if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil {
return err
}
if plainM2.State != StateM2 {
return NewResponseError(plainM1, plainM2)
}
return nil
}
func newPairingError(code byte) error {
var text string
// https://github.com/apple/HomeKitADK/blob/fb201f98f5fdc7fef6a455054f08b59cca5d1ec8/HAP/HAPPairing.h#L89
switch code {
case 1:
text = "Generic error to handle unexpected errors"
case 2:
text = "Setup code or signature verification failed"
case 3:
text = "Client must look at the retry delay TLV item and wait that many seconds before retrying"
case 4:
text = "Server cannot accept any more pairings"
case 5:
text = "Server reached its maximum number of authentication attempts"
case 6:
text = "Server pairing method is unavailable"
case 7:
text = "Server is busy and cannot accept a pairing request at this time"
default:
text = "Unknown pairing error"
}
return errors.New("hap: " + text)
}
func keyDerivativeFuncRFC2945(username []byte) srp.KeyDerivationFunc {
return func(salt, password []byte) []byte {
h1 := sha512.New()
h1.Write(username)
h1.Write([]byte(":"))
h1.Write(password)
h2 := sha512.New()
h2.Write(salt)
h2.Write(h1.Sum(nil))
return h2.Sum(nil)
}
}