package hap import ( "bufio" "crypto/sha512" "errors" "net" "net/url" "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/tadglines/go-pkgs/crypto/srp" ) // Pair homekit func Pair(rawURL string) (*Client, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err } query := u.Query() c := &Client{ DeviceAddress: u.Host, DeviceID: query.Get("device_id"), ClientID: query.Get("client_id"), ClientPrivate: DecodeKey(query.Get("client_private")), } if c.ClientID == "" { c.ClientID = GenerateUUID() } if c.ClientPrivate == nil { c.ClientPrivate = GenerateKey() } if err = c.Pair(query.Get("feature"), query.Get("pin")); err != nil { return nil, err } return c, nil } func Unpair(rawURL string) error { u, err := url.Parse(rawURL) if err != nil { return err } query := u.Query() conn := &Client{ DeviceAddress: u.Host, DeviceID: query.Get("device_id"), DevicePublic: DecodeKey(query.Get("device_public")), ClientID: query.Get("client_id"), ClientPrivate: DecodeKey(query.Get("client_private")), } if err = conn.Dial(); err != nil { return err } defer conn.Close() if err = conn.ListPairings(); err != nil { return err } return conn.DeletePairing(conn.ClientID) } func (c *Client) Pair(feature, pin string) (err error) { if pin, err = SanitizePin(pin); err != nil { return err } 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 feature == "1" { 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 string `tlv8:"2"` SessionKey string `tlv8:"3"` // server public key, aka session.B State byte `tlv8:"6"` Error byte `tlv8:"7"` } if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &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([]byte(plainM2.Salt), []byte(plainM2.SessionKey)) if err != nil { return } // STEP M3. Send request plainM3 := struct { SessionKey string `tlv8:"3"` Proof string `tlv8:"4"` State byte `tlv8:"6"` }{ SessionKey: string(session.GetA()), // client public key, aka session.A Proof: string(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 string `tlv8:"4"` // server proof State byte `tlv8:"6"` Error byte `tlv8:"7"` EncryptedData string `tlv8:"5"` // skip EncryptedData validation (for MFi devices) } if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &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([]byte(plainM4.Proof)) { return errors.New("hap: VerifyServerAuthenticator") } // 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 string `tlv8:"3"` Signature string `tlv8:"10"` }{ Identifier: c.ClientID, PublicKey: string(c.ClientPublic()), Signature: string(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 string `tlv8:"5"` State byte `tlv8:"6"` }{ EncryptedData: string(b), State: StateM5, } if res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(cipherM5)); err != nil { return } // STEP M6. Read response cipherM6 := struct { EncryptedData string `tlv8:"5"` State byte `tlv8:"6"` Error byte `tlv8:"7"` }{} if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &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", []byte(cipherM6.EncryptedData)) if err != nil { return } plainM6 := struct { Identifier string `tlv8:"1"` PublicKey string `tlv8:"3"` Signature string `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([]byte(plainM6.PublicKey), b, []byte(plainM6.Signature)) { return errors.New("hap: ValidateSignature") } if c.DeviceID != plainM6.Identifier { return errors.New("hap: wrong DeviceID: " + plainM6.Identifier) } c.DevicePublic = []byte(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 string `tlv8:"3"` State byte `tlv8:"6"` Permission byte `tlv8:"11"` } if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &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 string `tlv8:"3"` State byte `tlv8:"6"` Permission byte `tlv8:"11"` }{ Method: MethodAddPairing, Identifier: clientID, PublicKey: string(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, res.ContentLength, &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, res.ContentLength, &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) } }