Refactoring for HomeKit client

This commit is contained in:
Alexey Khit
2022-11-11 21:56:58 +03:00
parent 7bc3534bcb
commit 77888fe086
22 changed files with 300 additions and 275 deletions
+33
View File
@@ -0,0 +1,33 @@
# Home Accessory Protocol
> PS. Character = Characteristic
**Device** - HomeKit end device (swith, camera, etc)
- mDNS name: `MyCamera._hap._tcp.local.`
- DeviceID - mac-like: `0E:AA:CE:2B:35:71`
- HomeKit device is described by:
- one or more `Accessories` - has `AID` and `Services`
- `Services` - has `IID`, `Type` and `Characters`
- `Characters` - has `IID`, `Type`, `Format` and `Value`
**Client** - HomeKit client (iPhone, iPad, MacBook or opensource library)
- ClientID - static random UUID
- ClientPublic/ClientPrivate - static random 32 byte keypair
- can pair with Device (exchange ClientID/ClientPublic, ServerID/ServerPublic using Pin)
- can auth to Device using ClientPrivate
- holding persistant Secure connection to device
- can read device Accessories
- can read and write device Characters
- can subscribe on device Characters change (Event)
**Server** - HomeKit server (soft on end device or opensource library)
- ServerID - same as DeviceID (using for Client auth)
- ServerPublic/ServerPrivate - static random 32 byte keypair
## Useful links
- [Extracting HomeKit Pairing Keys](https://pvieito.com/2019/12/extract-homekit-pairing-keys)
- [HAP in AirPlay2 receiver](https://github.com/openairplay/airplay2-receiver/blob/master/ap2/pairing/hap.py)
+62
View File
@@ -0,0 +1,62 @@
package hap
type Accessory struct {
AID int `json:"aid"`
Services []*Service `json:"services"`
}
type Accessories struct {
Accessories []*Accessory `json:"accessories"`
}
type Characters struct {
Characters []*Character `json:"characteristics"`
}
func (a *Accessory) GetService(servType string) *Service {
for _, serv := range a.Services {
if serv.Type == servType {
return serv
}
}
return nil
}
func (a *Accessory) GetCharacter(charType string) *Character {
for _, serv := range a.Services {
for _, char := range serv.Characters {
if char.Type == charType {
return char
}
}
}
return nil
}
func (a *Accessory) GetCharacterByID(iid int) *Character {
for _, serv := range a.Services {
for _, char := range serv.Characters {
if char.IID == iid {
return char
}
}
}
return nil
}
type Service struct {
IID int `json:"iid"`
Type string `json:"type"`
Primary bool `json:"primary,omitempty"`
Hidden bool `json:"hidden,omitempty"`
Characters []*Character `json:"characteristics"`
}
func (s *Service) GetCharacter(charType string) *Character {
for _, char := range s.Characters {
if char.Type == charType {
return char
}
}
return nil
}
+100
View File
@@ -0,0 +1,100 @@
package camera
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/rtp"
)
type Client struct {
client *hap.Conn
}
func NewClient(client *hap.Conn) *Client {
return &Client{client: client}
}
func (c *Client) StartStream(ses *Session) (err error) {
// Step 1. Check if camera ready (free) to stream
var srv *hap.Service
if srv, err = c.GetFreeStream(); err != nil {
return err
}
if srv == nil {
return errors.New("no free streams")
}
if ses.Answer, err = c.SetupEndpoins(srv, ses.Offer); err != nil {
return
}
return c.SetConfig(srv, ses.Config)
}
// GetFreeStream search free streaming service.
// Usual every HomeKit camera can stream only to two clients simultaniosly.
// So it has two similar services for streaming.
func (c *Client) GetFreeStream() (srv *hap.Service, err error) {
var accs []*hap.Accessory
if accs, err = c.client.GetAccessories(); err != nil {
return
}
for _, srv = range accs[0].Services {
for _, char := range srv.Characters {
if char.Type == characteristic.TypeStreamingStatus {
status := rtp.StreamingStatus{}
if err = char.ReadTLV8(&status); err != nil {
return
}
if status.Status == rtp.SessionStatusSuccess {
return
}
}
}
}
return nil, nil
}
func (c *Client) SetupEndpoins(
srv *hap.Service, req *rtp.SetupEndpoints,
) (res *rtp.SetupEndpointsResponse, err error) {
// get setup endpoint character ID
char := srv.GetCharacter(characteristic.TypeSetupEndpoints)
char.Event = nil
// encode new character value
if err = char.Write(req); err != nil {
return
}
// write (put) new endpoint value to device
if err = c.client.PutCharacters(char); err != nil {
return
}
// get new endpoint value from device (response)
if err = c.client.GetCharacter(char); err != nil {
return
}
// decode new endpoint value
res = &rtp.SetupEndpointsResponse{}
if err = char.ReadTLV8(res); err != nil {
return
}
return
}
func (c *Client) SetConfig(srv *hap.Service, config *rtp.StreamConfiguration) (err error) {
// get setup endpoint character ID
char := srv.GetCharacter(characteristic.TypeSelectedStreamConfiguration)
char.Event = nil
// encode new character value
if err = char.Write(config); err != nil {
panic(err)
}
// write (put) new character value to device
return c.client.PutCharacters(char)
}
+75
View File
@@ -0,0 +1,75 @@
package camera
import (
cryptorand "crypto/rand"
"encoding/binary"
"github.com/brutella/hap/rtp"
)
type Session struct {
Offer *rtp.SetupEndpoints
Answer *rtp.SetupEndpointsResponse
Config *rtp.StreamConfiguration
}
func NewSession(vp *rtp.VideoParameters, ap *rtp.AudioParameters) *Session {
vp.RTP = rtp.RTPParams{
PayloadType: 99,
Ssrc: RandomUint32(),
Bitrate: 2048,
Interval: 10,
MTU: 1200, // like WebRTC
}
ap.RTP = rtp.RTPParams{
PayloadType: 110,
Ssrc: RandomUint32(),
Bitrate: 32,
Interval: 10,
ComfortNoisePayloadType: 98,
MTU: 0,
}
sessionID := RandomBytes(16)
s := &Session{
Offer: &rtp.SetupEndpoints{
SessionId: sessionID,
Video: rtp.CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
Audio: rtp.CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
},
Config: &rtp.StreamConfiguration{
Command: rtp.SessionControlCommand{
Identifier: sessionID,
Type: rtp.SessionControlCommandTypeStart,
},
Video: *vp,
Audio: *ap,
},
}
return s
}
func (s *Session) SetLocalEndpoint(host string, port uint16) {
s.Offer.ControllerAddr = rtp.Addr{
IPAddr: host,
VideoRtpPort: port,
AudioRtpPort: port,
}
}
func RandomBytes(size int) []byte {
data := make([]byte, size)
_, _ = cryptorand.Read(data)
return data
}
func RandomUint32() uint32 {
data := make([]byte, 4)
_, _ = cryptorand.Read(data)
return binary.BigEndian.Uint32(data)
}
+133
View File
@@ -0,0 +1,133 @@
package hap
import (
"bytes"
"encoding/base64"
"encoding/json"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/tlv8"
"io"
"net/http"
)
type Character struct {
AID int `json:"aid,omitempty"`
IID int `json:"iid"`
Type string `json:"type,omitempty"`
Format string `json:"format,omitempty"`
Value interface{} `json:"value,omitempty"`
Event interface{} `json:"ev,omitempty"`
Perms []string `json:"perms,omitempty"`
Description string `json:"description,omitempty"`
//MaxDataLen int `json:"maxDataLen"`
listeners map[io.Writer]bool
}
func (c *Character) AddListener(w io.Writer) {
// TODO: sync.Mutex
if c.listeners == nil {
c.listeners = map[io.Writer]bool{}
}
c.listeners[w] = true
}
func (c *Character) RemoveListener(w io.Writer) {
delete(c.listeners, w)
if len(c.listeners) == 0 {
c.listeners = nil
}
}
func (c *Character) NotifyListeners(ignore io.Writer) error {
if c.listeners == nil {
return nil
}
data, err := c.GenerateEvent()
if err != nil {
return err
}
for w, _ := range c.listeners {
if w == ignore {
continue
}
if _, err = w.Write(data); err != nil {
// error not a problem - just remove listener
c.RemoveListener(w)
}
}
return nil
}
// GenerateEvent with raw HTTP headers
func (c *Character) GenerateEvent() (data []byte, err error) {
chars := Characters{
Characters: []*Character{{AID: DeviceAID, IID: c.IID, Value: c.Value}},
}
if data, err = json.Marshal(chars); err != nil {
return
}
res := http.Response{
StatusCode: http.StatusOK,
ProtoMajor: 1,
ProtoMinor: 0,
Header: http.Header{"Content-Type": []string{MimeJSON}},
ContentLength: int64(len(data)),
Body: io.NopCloser(bytes.NewReader(data)),
}
buf := bytes.NewBuffer([]byte{0})
if err = res.Write(buf); err != nil {
return
}
copy(buf.Bytes(), "EVENT")
return buf.Bytes(), err
}
// Set new value and NotifyListeners
func (c *Character) Set(v interface{}) (err error) {
if err = c.Write(v); err != nil {
return
}
return c.NotifyListeners(nil)
}
// Write new value with right format
func (c *Character) Write(v interface{}) (err error) {
switch c.Format {
case characteristic.FormatTLV8:
var data []byte
if data, err = tlv8.Marshal(v); err != nil {
return
}
c.Value = base64.StdEncoding.EncodeToString(data)
case characteristic.FormatBool:
switch v.(type) {
case bool:
c.Value = v.(bool)
case float64:
c.Value = v.(float64) != 0
}
}
return
}
// ReadTLV8 value to right struct
func (c *Character) ReadTLV8(v interface{}) (err error) {
var data []byte
if data, err = base64.StdEncoding.DecodeString(c.Value.(string)); err != nil {
return
}
return tlv8.Unmarshal(data, v)
}
func (c *Character) ReadBool() bool {
return c.Value.(bool)
}
+733
View File
@@ -0,0 +1,733 @@
package hap
import (
"bufio"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/brutella/hap"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/curve25519"
"github.com/brutella/hap/ed25519"
"github.com/brutella/hap/hkdf"
"github.com/brutella/hap/tlv8"
"github.com/tadglines/go-pkgs/crypto/srp"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
)
// Conn for HomeKit. DevicePublic can be null.
type Conn struct {
streamer.Element
DeviceAddress string // including port
DeviceID string
DevicePublic []byte
ClientID string
ClientPrivate []byte
OnEvent func(res *http.Response)
Output func(msg interface{})
conn net.Conn
secure *Secure
httpResponse chan *bufio.Reader
}
func NewConn(rawURL string) (*Conn, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
query := u.Query()
c := &Conn{
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")),
}
return c, nil
}
func Pair(deviceID, pin string) (*Conn, error) {
entry := mdns.GetEntry(deviceID)
if entry == nil {
return nil, errors.New("can't find device via mDNS")
}
c := &Conn{
DeviceAddress: fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port),
DeviceID: deviceID,
ClientID: GenerateUUID(),
ClientPrivate: GenerateKey(),
}
var mfi bool
for _, field := range entry.InfoFields {
if field[:2] == "ff" {
if field[3] == '1' {
mfi = true
}
break
}
}
return c, c.Pair(mfi, pin)
}
func (c *Conn) ClientPublic() []byte {
return c.ClientPrivate[32:]
}
func (c *Conn) URL() string {
return fmt.Sprintf(
"homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x",
c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate,
)
}
func (c *Conn) DialAndServe() error {
if err := c.Dial(); err != nil {
return err
}
return c.Handle()
}
func (c *Conn) Dial() error {
// update device host before dial
if host := mdns.GetAddress(c.DeviceID); host != "" {
c.DeviceAddress = host
}
var err error
c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, time.Second*5)
if err != nil {
return err
}
// STEP M1: send our session public to device
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
// 1. generate payload
// important not include other fields
requestM1 := struct {
State byte `tlv8:"6"`
PublicKey []byte `tlv8:"3"`
}{
State: hap.M1,
PublicKey: sessionPublic[:],
}
// 2. pack payload to TLV8
buf, err := tlv8.Marshal(requestM1)
if err != nil {
return err
}
// 3. send request
resp, err := c.Post(UriPairVerify, buf)
if err != nil {
return err
}
// STEP M2: unpack deviceID from response
responseM2 := PairVerifyPayload{}
if err = tlv8.UnmarshalReader(resp.Body, &responseM2); err != nil {
return err
}
// 1. generate session shared key
var deviceSessionPublic [32]byte
copy(deviceSessionPublic[:], responseM2.PublicKey)
sessionShared := curve25519.SharedSecret(sessionPrivate, deviceSessionPublic)
sessionKey, err := hkdf.Sha512(
sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"),
[]byte("Pair-Verify-Encrypt-Info"),
)
// 2. decrypt M2 response with session key
msg := responseM2.EncryptedData[:len(responseM2.EncryptedData)-16]
var mac [16]byte
copy(mac[:], responseM2.EncryptedData[len(msg):]) // 16 byte (MAC)
buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PV-Msg02"), msg, mac, nil,
)
// 3. unpack payload from TLV8
payloadM2 := PairVerifyPayload{}
if err = tlv8.Unmarshal(buf, &payloadM2); err != nil {
return err
}
// 4. verify signature for M2 response with device public
// device session + device id + our session
if c.DevicePublic != nil {
buf = nil
buf = append(buf, responseM2.PublicKey[:]...)
buf = append(buf, []byte(payloadM2.Identifier)...)
buf = append(buf, sessionPublic[:]...)
if !ed25519.ValidateSignature(
c.DevicePublic[:], buf, payloadM2.Signature,
) {
return errors.New("device public signature invalid")
}
}
// STEP M3: send our clientID to device
// 1. generate signature with our private key
// (our session + our ID + device session)
buf = nil
buf = append(buf, sessionPublic[:]...)
buf = append(buf, []byte(c.ClientID)...)
buf = append(buf, responseM2.PublicKey[:]...)
signature, err := ed25519.Signature(c.ClientPrivate[:], buf)
if err != nil {
return err
}
// 2. generate payload
payloadM3 := struct {
Identifier string `tlv8:"1"`
Signature []byte `tlv8:"10"`
}{
Identifier: c.ClientID,
Signature: signature,
}
// 3. pack payload to TLV8
buf, err = tlv8.Marshal(payloadM3)
if err != nil {
return err
}
// 4. encrypt payload with session key
msg, mac, _ = chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PV-Msg03"), buf, nil,
)
// 4. generate request
requestM3 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
State: hap.M3,
EncryptedData: append(msg, mac[:]...),
}
// 5. pack payload to TLV8
buf, err = tlv8.Marshal(requestM3)
if err != nil {
return err
}
resp, err = c.Post(UriPairVerify, buf)
if err != nil {
return err
}
// STEP M4. Read response
responseM4 := PairVerifyPayload{}
if err = tlv8.UnmarshalReader(resp.Body, &responseM4); err != nil {
return err
}
// 1. check response state
if responseM4.State != 4 || responseM4.Status != 0 {
return fmt.Errorf("wrong M4 response: %+v", responseM4)
}
c.secure, err = NewSecure(sessionShared, false)
//c.secure.Buffer = bytes.NewBuffer(nil)
c.secure.Conn = c.conn
c.httpResponse = make(chan *bufio.Reader, 10)
return err
}
// https://github.com/apple/HomeKitADK/blob/master/HAP/HAPPairingPairSetup.c
func (c *Conn) 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:]
c.conn, err = net.Dial("tcp", c.DeviceAddress)
if err != nil {
return
}
// STEP M1. Generate request
reqM1 := struct {
Method byte `tlv8:"0"`
State byte `tlv8:"6"`
}{
State: hap.M1,
}
if mfi {
reqM1.Method = 1 // ff=1 => method=1, ff=2 => method=0
}
buf, err := tlv8.Marshal(reqM1)
if err != nil {
return
}
// STEP M1. Send request
res, err := c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M2. Read response
resM2 := struct {
Salt []byte `tlv8:"2"`
PublicKey []byte `tlv8:"3"` // server public key, aka session.B
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM2); err != nil {
return
}
if resM2.State != 2 || resM2.Error > 0 {
return fmt.Errorf("wrong M2: %+v", resM2)
}
// STEP M3. Generate session using pin
username := []byte("Pair-Setup")
SRP, err := srp.NewSRP(
"rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username),
)
if err != nil {
return
}
SRP.SaltLength = 16
// username: "Pair-Setup"
// password: PIN (with dashes)
session := SRP.NewClientSession(username, []byte(pin))
sessionShared, err := session.ComputeKey(resM2.Salt, resM2.PublicKey)
if err != nil {
return
}
// STEP M3. Generate request
reqM3 := struct {
PublicKey []byte `tlv8:"3"`
Proof []byte `tlv8:"4"`
State byte `tlv8:"6"`
}{
PublicKey: session.GetA(), // client public key, aka session.A
Proof: session.ComputeAuthenticator(),
State: hap.M3,
}
buf, err = tlv8.Marshal(reqM3)
if err != nil {
return err
}
// STEP M3. Send request
res, err = c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M4. Read response
resM4 := struct {
Proof []byte `tlv8:"4"` // server proof
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM4); err != nil {
return
}
if resM4.Error == 2 {
return fmt.Errorf("wrong PIN: %s", pin)
}
if resM4.State != 4 || resM4.Error > 0 {
return fmt.Errorf("wrong M4: %+v", resM4)
}
// STEP M4. Verify response
if !session.VerifyServerAuthenticator(resM4.Proof) {
return errors.New("verify server auth fail")
}
// STEP M5. Generate signature
saltKey, err := hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"),
[]byte("Pair-Setup-Controller-Sign-Info"),
)
if err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, []byte(c.ClientID)...)
buf = append(buf, c.ClientPublic()...)
signature, err := ed25519.Signature(c.ClientPrivate, buf)
if err != nil {
return
}
// STEP M5. Generate payload
msgM5 := struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{
Identifier: c.ClientID,
PublicKey: c.ClientPublic(),
Signature: signature,
}
buf, err = tlv8.Marshal(msgM5)
if err != nil {
return
}
// STEP M5. Encrypt payload
sessionKey, err := hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Encrypt-Salt"),
[]byte("Pair-Setup-Encrypt-Info"),
)
if err != nil {
return
}
buf, mac, _ := chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PS-Msg05"), buf, nil,
)
// STEP M5. Generate request
reqM5 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
EncryptedData: append(buf, mac[:]...),
State: hap.M5,
}
buf, err = tlv8.Marshal(reqM5)
if err != nil {
return err
}
// STEP M5. Send request
res, err = c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M6. Read response
resM6 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM6); err != nil {
return
}
if resM6.State != 6 || resM6.Error > 0 {
return fmt.Errorf("wrong M6: %+v", resM2)
}
// STEP M6. Decrypt payload
msg := resM6.EncryptedData[:len(resM6.EncryptedData)-16]
copy(mac[:], resM6.EncryptedData[len(msg):]) // 16 byte (MAC)
buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PS-Msg06"), msg, mac, nil,
)
if err != nil {
return
}
msgM6 := struct {
Identifier []byte `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{}
if err = tlv8.Unmarshal(buf, &msgM6); err != nil {
return
}
// STEP M6. Verify payload
if saltKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"),
[]byte("Pair-Setup-Accessory-Sign-Info"),
); err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, msgM6.Identifier...)
buf = append(buf, msgM6.PublicKey...)
if !ed25519.ValidateSignature(
msgM6.PublicKey[:], buf, msgM6.Signature,
) {
return errors.New("wrong server signature")
}
if c.DeviceID != string(msgM6.Identifier) {
return fmt.Errorf("wrong Device ID: %s", msgM6.Identifier)
}
c.DevicePublic = msgM6.PublicKey
return nil
}
func (c *Conn) Close() error {
if c.conn == nil {
return nil
}
conn := c.conn
c.conn = nil
return conn.Close()
}
func (c *Conn) GetAccessories() ([]*Accessory, error) {
res, err := c.Get("/accessories")
if err != nil {
return nil, err
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
p := Accessories{}
if err = json.Unmarshal(data, &p); err != nil {
return nil, err
}
for _, accs := range p.Accessories {
for _, serv := range accs.Services {
for _, char := range serv.Characters {
char.AID = accs.AID
}
}
}
return p.Accessories, nil
}
func (c *Conn) GetCharacters(query string) ([]*Character, error) {
res, err := c.Get("/characteristics?id=" + query)
if err != nil {
return nil, err
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
ch := Characters{}
if err = json.Unmarshal(data, &ch); err != nil {
return nil, err
}
return ch.Characters, nil
}
func (c *Conn) GetCharacter(char *Character) error {
query := fmt.Sprintf("%d.%d", char.AID, char.IID)
chars, err := c.GetCharacters(query)
if err != nil {
return err
}
char.Value = chars[0].Value
return nil
}
func (c *Conn) PutCharacters(characters ...*Character) (err error) {
for i, char := range characters {
if char.Event != nil {
char = &Character{AID: char.AID, IID: char.IID, Event: char.Event}
} else {
char = &Character{AID: char.AID, IID: char.IID, Value: char.Value}
}
characters[i] = char
}
var data []byte
if data, err = json.Marshal(Characters{characters}); err != nil {
return
}
var res *http.Response
if res, err = c.Put("/characteristics", data); err != nil {
return
}
if res.StatusCode >= 400 {
return errors.New("wrong response status")
}
return
}
func (c *Conn) GetImage(width, height int) ([]byte, error) {
res, err := c.Post(
"/resource", []byte(fmt.Sprintf(
`{"image-width":%d,"image-height":%d,"resource-type":"image","reason":0}`,
width, height,
)),
)
if err != nil {
return nil, err
}
return io.ReadAll(res.Body)
}
//func (c *Client) onEventData(r io.Reader) error {
// if c.OnEvent == nil {
// return nil
// }
//
// data, err := io.ReadAll(r)
//
// ch := Characters{}
// if err = json.Unmarshal(data, &ch); err != nil {
// return err
// }
//
// c.OnEvent(ch.Characters)
//
// return nil
//}
func (c *Conn) ListPairings() error {
pReq := struct {
Method byte `tlv8:"0"`
State byte `tlv8:"6"`
}{
Method: hap.MethodListPairings,
State: hap.M1,
}
data, err := tlv8.Marshal(pReq)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
// TODO: don't know how to fix array of items
var pRes struct {
State byte `tlv8:"6"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Permission byte `tlv8:"11"`
}
if err = tlv8.Unmarshal(data, &pRes); err != nil {
return err
}
return nil
}
func (c *Conn) PairingsAdd(clientID string, clientPublic []byte, admin bool) error {
pReq := struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
State byte `tlv8:"6"`
Permission byte `tlv8:"11"`
}{
Method: hap.MethodAddPairing,
Identifier: clientID,
PublicKey: clientPublic,
State: hap.M1,
Permission: hap.PermissionUser,
}
if admin {
pReq.Permission = hap.PermissionAdmin
}
data, err := tlv8.Marshal(pReq)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
var pRes struct {
State byte `tlv8:"6"`
Unknown byte `tlv8:"7"`
}
if err = tlv8.Unmarshal(data, &pRes); err != nil {
return err
}
return nil
}
func (c *Conn) DeletePairing(id string) error {
reqM1 := struct {
State byte `tlv8:"6"`
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
}{
State: hap.M1,
Method: hap.MethodDeletePairing,
Identifier: id,
}
data, err := tlv8.Marshal(reqM1)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
var resM2 struct {
State byte `tlv8:"6"`
}
if err = tlv8.Unmarshal(data, &resM2); err != nil {
return err
}
if resM2.State != hap.M2 {
return errors.New("wrong state")
}
return nil
}
func (c *Conn) LocalAddr() string {
return c.conn.LocalAddr().String()
}
func DecodeKey(s string) []byte {
if s == "" {
return nil
}
data, err := hex.DecodeString(s)
if err != nil {
return nil
}
return data
}
+90
View File
@@ -0,0 +1,90 @@
package hap
import (
"crypto/rand"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
)
const DeviceAID = 1 // TODO: fix someday
func GenerateID(name string) string {
sum := sha512.Sum512([]byte(name))
return fmt.Sprintf(
"%02X:%02X:%02X:%02X:%02X:%02X",
sum[0], sum[1], sum[2], sum[3], sum[4], sum[5],
)
}
func GenerateUUID() string {
//12345678-9012-3456-7890-123456789012
data := make([]byte, 16)
_, _ = rand.Read(data)
s := hex.EncodeToString(data)
return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:]
}
type PairVerifyPayload struct {
Method byte `tlv8:"0,optional"`
Identifier string `tlv8:"1,optional"`
PublicKey []byte `tlv8:"3,optional"`
EncryptedData []byte `tlv8:"5,optional"`
State byte `tlv8:"6,optional"`
Status byte `tlv8:"7,optional"`
Signature []byte `tlv8:"10,optional"`
}
//func (c *Character) Unmarshal(value interface{}) error {
// switch c.Format {
// case characteristic.FormatTLV8:
// data, err := base64.StdEncoding.DecodeString(c.Value.(string))
// if err != nil {
// return err
// }
// return tlv8.Unmarshal(data, value)
// }
// return nil
//}
//func (c *Character) Marshal(value interface{}) error {
// switch c.Format {
// case characteristic.FormatTLV8:
// data, err := tlv8.Marshal(value)
// if err != nil {
// return err
// }
// c.Value = base64.StdEncoding.EncodeToString(data)
// }
// return nil
//}
func (c *Character) String() string {
data, err := json.Marshal(c)
if err != nil {
return "ERROR"
}
return string(data)
}
func UnmarshalEvent(res *http.Response) (char *Character, err error) {
var data []byte
if data, err = io.ReadAll(res.Body); err != nil {
return
}
ch := Characters{}
if err = json.Unmarshal(data, &ch); err != nil {
return
}
if len(ch.Characters) > 1 {
panic("not implemented")
}
char = ch.Characters[0]
return
}
+246
View File
@@ -0,0 +1,246 @@
package hap
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net/http"
"net/textproto"
"strconv"
)
const (
MimeTLV8 = "application/pairing+tlv8"
MimeJSON = "application/hap+json"
UriPairSetup = "/pair-setup"
UriPairVerify = "/pair-verify"
UriPairings = "/pairings"
UriAccessories = "/accessories"
UriCharacteristics = "/characteristics"
UriResource = "/resource"
)
func (c *Conn) Write(p []byte) (r io.Reader, err error) {
if c.secure == nil {
if _, err = c.conn.Write(p); err == nil {
r = bufio.NewReader(c.conn)
}
} else {
if _, err = c.secure.Write(p); err == nil {
r = <-c.httpResponse
}
}
return
}
func (c *Conn) Do(req *http.Request) (*http.Response, error) {
if c.secure == nil {
// insecure requests
if err := req.Write(c.conn); err != nil {
return nil, err
}
return http.ReadResponse(bufio.NewReader(c.conn), req)
}
// secure support write interface to connection
if err := req.Write(c.secure); err != nil {
return nil, err
}
// get decrypted buffer from connection
buf := <-c.httpResponse
return http.ReadResponse(buf, req)
}
func (c *Conn) Get(uri string) (*http.Response, error) {
req, err := http.NewRequest(
"GET", "http://"+c.DeviceAddress+uri, nil,
)
if err != nil {
return nil, err
}
return c.Do(req)
}
func (c *Conn) Post(uri string, data []byte) (*http.Response, error) {
req, err := http.NewRequest(
"POST", "http://"+c.DeviceAddress+uri,
bytes.NewReader(data),
)
if err != nil {
return nil, err
}
switch uri {
case "/pair-verify", "/pairings":
req.Header.Set("Content-Type", MimeTLV8)
case UriResource:
req.Header.Set("Content-Type", MimeJSON)
}
return c.Do(req)
}
func (c *Conn) Put(uri string, data []byte) (*http.Response, error) {
req, err := http.NewRequest(
"PUT", "http://"+c.DeviceAddress+uri,
bytes.NewReader(data),
)
if err != nil {
return nil, err
}
switch uri {
case UriCharacteristics:
req.Header.Set("Content-Type", MimeJSON)
}
return c.Do(req)
}
func (c *Conn) Handle() (err error) {
defer func() {
if c.conn == nil {
err = nil
}
}()
b := make([]byte, 512000)
for {
var total, content int
header := -1
for {
var n1 int
n1, err = c.secure.Read(b[total:])
if err != nil {
return err
}
if n1 == 0 {
return io.EOF
}
total += n1
// TODO: rewrite
if header == -1 {
// step 1. wait whole header
header = bytes.Index(b[:total], []byte("\r\n\r\n"))
if header < 0 {
continue
}
header += 4
// step 2. check content-length
i1 := bytes.Index(b[:total], []byte("Content-Length: "))
if i1 < 0 {
break
}
i1 += 16
i2 := bytes.IndexByte(b[i1:total], '\r')
content, err = strconv.Atoi(string(b[i1 : i1+i2]))
if err != nil {
break
}
}
if total >= header+content {
break
}
}
// copy slice to buffer
buf := bytes.NewBuffer(make([]byte, 0, total))
buf.Write(b[:total])
r := bufio.NewReader(buf)
// EVENT/1.0 200 OK
if b[0] == 'E' {
if c.OnEvent == nil {
continue
}
tp := textproto.NewReader(r)
var s string
if s, err = tp.ReadLine(); err != nil {
return err
}
if s != "EVENT/1.0 200 OK" {
return errors.New("wrong response")
}
var mimeHeader textproto.MIMEHeader
if mimeHeader, err = tp.ReadMIMEHeader(); err != nil {
return err
}
var cl int
if cl, err = strconv.Atoi(
mimeHeader.Get("Content-Length"),
); err != nil {
return err
}
res := http.Response{
StatusCode: 200,
Proto: "EVENT/1.0",
ProtoMajor: 1,
ProtoMinor: 0,
Header: http.Header(mimeHeader),
ContentLength: int64(cl),
Body: io.NopCloser(r),
}
c.OnEvent(&res)
continue
}
//if bytes.Index(b, []byte("image/jpeg")) > 0 {
// if total, err = c.secure.Read(b); err != nil {
// return
// }
// buf.Write(b[:total])
//}
c.httpResponse <- r
}
}
func WriteStatusCode(w io.Writer, statusCode int) (err error) {
body := []byte(fmt.Sprintf(
"HTTP/1.1 %d %s\n\n", statusCode, http.StatusText(statusCode),
))
//print("<<<", string(body), "<<<\n")
_, err = w.Write(body)
return
}
func WriteResponse(
w io.Writer, statusCode int, contentType string, body []byte,
) (err error) {
header := fmt.Sprintf(
"HTTP/1.1 %d %s\nContent-Type: %s\nContent-Length: %d\n\n",
statusCode, http.StatusText(statusCode), contentType, len(body),
)
body = append([]byte(header), body...)
//print("<<<", string(body), "<<<\n")
_, err = w.Write(body)
return
}
func WriteChunked(w io.Writer, contentType string, body []byte) (err error) {
header := fmt.Sprintf(
"HTTP/1.1 200 OK\nContent-Type: %s\nTransfer-Encoding: chunked\n\n%x\n",
contentType, len(body),
)
body = append([]byte(header), body...)
body = append(body, "\n0\n\n"...)
//print("<<<", string(body), "<<<\n")
_, err = w.Write(body)
return
}
+42
View File
@@ -0,0 +1,42 @@
package mdns
import (
"fmt"
"github.com/hashicorp/mdns"
"strings"
)
const Suffix = "._hap._tcp.local."
func GetAll() chan *mdns.ServiceEntry {
entries := make(chan *mdns.ServiceEntry)
params := &mdns.QueryParam{
Service: "_hap._tcp", Entries: entries, DisableIPv6: true,
}
go func() {
_ = mdns.Query(params)
close(entries)
}()
return entries
}
func GetAddress(deviceID string) string {
for entry := range GetAll() {
if strings.Contains(entry.Info, deviceID) {
return fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port)
}
}
return ""
}
func GetEntry(deviceID string) *mdns.ServiceEntry {
for entry := range GetAll() {
if strings.Contains(entry.Info, deviceID) {
return entry
}
}
return nil
}
+53
View File
@@ -0,0 +1,53 @@
package mdns
import (
"github.com/hashicorp/mdns"
"net"
)
const HostHeaderTail = "._hap._tcp.local"
func NewServer(name string, port int, ips []net.IP, txt []string) (*mdns.Server, error) {
if ips == nil || ips[0] == nil {
ips = LocalIPs()
}
// important to set hostName manually with any value and `.local.` tail
// important to set ips manually
service, _ := mdns.NewMDNSService(
name, "_hap._tcp", "", name+".local.", port, ips, txt,
)
return mdns.NewServer(&mdns.Config{Zone: service})
}
func LocalIPs() []net.IP {
ifaces, err := net.Interfaces()
if err != nil {
return nil
}
var ips []net.IP
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 {
continue // interface down
}
if iface.Flags&net.FlagLoopback != 0 {
continue // loopback interface
}
var addrs []net.Addr
if addrs, err = iface.Addrs(); err != nil {
continue
}
for _, addr := range addrs {
switch addr := addr.(type) {
case *net.IPNet:
ips = append(ips, addr.IP)
case *net.IPAddr:
ips = append(ips, addr.IP)
}
}
}
return ips
}
+410
View File
@@ -0,0 +1,410 @@
package hap
import (
"bufio"
"crypto/sha512"
"errors"
"github.com/brutella/hap"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/curve25519"
"github.com/brutella/hap/ed25519"
"github.com/brutella/hap/hkdf"
"github.com/brutella/hap/tlv8"
"github.com/tadglines/go-pkgs/crypto/srp"
"net"
"net/http"
)
type pairSetupPayload struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
Salt []byte `tlv8:"2"`
PublicKey []byte `tlv8:"3"`
Proof []byte `tlv8:"4"`
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
RetryDelay byte `tlv8:"8"`
Certificate []byte `tlv8:"9"`
Signature []byte `tlv8:"10"`
Permissions byte `tlv8:"11"`
FragmentData []byte `tlv8:"13"`
FragmentLast []byte `tlv8:"14"`
}
func (s *Server) PairSetupHandler(
conn net.Conn, req *http.Request,
) (clientID string, err error) {
// STEP 1. Request from iPhone
payloadM1 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &payloadM1); err != nil {
return
}
if payloadM1.State != hap.M1 {
err = errors.New("wrong state")
return
}
// generate our session public and salt using PIN
username := []byte("Pair-Setup")
var SRP *srp.SRP
if SRP, err = srp.NewSRP(
"rfc5054.3072", sha512.New,
keyDerivativeFuncRFC2945(username),
); err != nil {
return
}
SRP.SaltLength = 16
var salt, verifier []byte
if salt, verifier, err = SRP.ComputeVerifier([]byte(s.Pin)); err != nil {
return
}
session := SRP.NewServerSession(username, salt, verifier)
// STEP 2. Response to iPhone
payloadM2 := struct {
Salt []byte `tlv8:"2"`
PublicKey []byte `tlv8:"3"`
State byte `tlv8:"6"`
}{
State: hap.M2,
PublicKey: session.GetB(),
Salt: salt,
}
var buf []byte
if buf, err = tlv8.Marshal(payloadM2); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
// STEP 3. Request from iPhone
r := bufio.NewReader(conn)
if req, err = http.ReadRequest(r); err != nil {
return
}
payloadM3 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &payloadM3); err != nil {
return
}
if payloadM3.State != hap.M3 {
err = errors.New("wrong state")
return
}
// important to compute key before verify client
var sessionShared []byte
if sessionShared, err = session.ComputeKey(payloadM3.PublicKey); err != nil {
return
}
// support skip pin verify (any pin accepted)
if s.Pin != "" && !session.VerifyClientAuthenticator(payloadM3.Proof) {
err = errors.New("client proof is invalid")
return
}
serverProof := session.ComputeAuthenticator(payloadM3.Proof)
// STEP 4. Response to iPhone
payloadM4 := struct {
Proof []byte `tlv8:"4"`
State byte `tlv8:"6"`
}{
State: hap.M4, Proof: serverProof,
}
if buf, err = tlv8.Marshal(payloadM4); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
// STEP 5. Request from iPhone
if req, err = http.ReadRequest(r); err != nil {
return
}
encryptedM5 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &encryptedM5); err != nil {
return
}
if encryptedM5.State != hap.M5 {
err = errors.New("wrong state")
return
}
msg := encryptedM5.EncryptedData[:len(encryptedM5.EncryptedData)-16]
var mac [16]byte
copy(mac[:], encryptedM5.EncryptedData[len(msg):]) // 16 byte (MAC)
// decrypt message using session shared
var sessionKey [32]byte
if sessionKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Encrypt-Salt"),
[]byte("Pair-Setup-Encrypt-Info"),
); err != nil {
return
}
if buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PS-Msg05"), msg, mac, nil,
); err != nil {
return
}
// unpack message from TLV8
payloadM5 := struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{}
if err = tlv8.Unmarshal(buf, &payloadM5); err != nil {
return
}
// 3. verify client ID and Public
var saltKey [32]byte
if saltKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"),
[]byte("Pair-Setup-Controller-Sign-Info"),
); err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, payloadM5.Identifier...)
buf = append(buf, payloadM5.PublicKey[:]...)
if !ed25519.ValidateSignature(
payloadM5.PublicKey[:], buf, payloadM5.Signature,
) {
err = errors.New("wrong client signature")
return
}
// 4. generate signature to our ID adn Public
if saltKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"),
[]byte("Pair-Setup-Accessory-Sign-Info"),
); err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, []byte(s.ServerID)...)
buf = append(buf, s.ServerPrivate[32:]...) // ServerPublic
var signature []byte
if signature, err = ed25519.Signature(s.ServerPrivate, buf); err != nil {
return
}
// 5. pack our ID and Public
payloadM6 := struct {
Identifier []byte `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{
Identifier: []byte(s.ServerID),
PublicKey: s.ServerPrivate[32:],
Signature: signature,
}
if buf, err = tlv8.Marshal(payloadM6); err != nil {
return
}
// 6. encrypt message
buf, mac, _ = chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PS-Msg06"), buf, nil,
)
// STEP 6. Response to iPhone
encryptedM6 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
State: hap.M6,
EncryptedData: append(buf, mac[:]...),
}
if buf, err = tlv8.Marshal(encryptedM6); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
if s.Pairings != nil {
s.Pairings[payloadM5.Identifier] = append(
payloadM5.PublicKey, 1, // adds admin (1) flag
)
}
clientID = payloadM5.Identifier
return
}
func keyDerivativeFuncRFC2945(username []byte) srp.KeyDerivationFunc {
return func(salt, pin []byte) []byte {
h := sha512.New()
h.Write(username)
h.Write([]byte(":"))
h.Write(pin)
t2 := h.Sum(nil)
h.Reset()
h.Write(salt)
h.Write(t2)
return h.Sum(nil)
}
}
type pairVerifyPayload struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Signature []byte `tlv8:"10"`
}
func (s *Server) PairVerifyHandler(
conn net.Conn, req *http.Request,
) (secure *Secure, err error) {
// STEP M1. Request from iPhone
payloadM1 := pairVerifyPayload{}
if err = tlv8.UnmarshalReader(req.Body, &payloadM1); err != nil {
return
}
if payloadM1.State != hap.M1 {
err = errors.New("wrong state")
return
}
var clientPublic [32]byte
copy(clientPublic[:], payloadM1.PublicKey)
// Generate the key pair.
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
sessionShared := curve25519.SharedSecret(sessionPrivate, clientPublic)
var sessionKey [32]byte
if sessionKey, err = hkdf.Sha512(
sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"),
[]byte("Pair-Verify-Encrypt-Info"),
); err != nil {
return
}
var buf []byte
buf = append(buf, sessionPublic[:]...)
buf = append(buf, s.ServerID...)
buf = append(buf, clientPublic[:]...)
var signature []byte
if signature, err = ed25519.Signature(s.ServerPrivate[:], buf); err != nil {
return
}
// STEP M2. Response to iPhone
payloadM2 := struct {
Identifier string `tlv8:"1"`
Signature []byte `tlv8:"10"`
}{
Identifier: s.ServerID,
Signature: signature,
}
if buf, err = tlv8.Marshal(payloadM2); err != nil {
return
}
var mac [16]byte
buf, mac, _ = chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PV-Msg02"), buf, nil,
)
encryptedM2 := struct {
State byte `tlv8:"6"`
PublicKey []byte `tlv8:"3"`
EncryptedData []byte `tlv8:"5"`
}{
State: hap.M2,
PublicKey: sessionPublic[:],
EncryptedData: append(buf, mac[:]...),
}
if buf, err = tlv8.Marshal(encryptedM2); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
// STEP M3. Request from iPhone
r := bufio.NewReader(conn)
if req, err = http.ReadRequest(r); err != nil {
return
}
encryptedM3 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &encryptedM3); err != nil {
return
}
if encryptedM3.State != hap.M3 {
err = errors.New("wrong state")
return
}
buf = encryptedM3.EncryptedData[:len(encryptedM3.EncryptedData)-16]
copy(mac[:], encryptedM3.EncryptedData[len(buf):]) // 16 byte (MAC)
if buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PV-Msg03"), buf, mac, nil,
); err != nil {
return
}
payloadM3 := pairVerifyPayload{}
if err = tlv8.Unmarshal(buf, &payloadM3); err != nil {
return
}
if s.Pairings != nil {
pairing := s.Pairings[payloadM3.Identifier]
if pairing == nil {
err = errors.New("not paired yet")
return
}
buf = nil
buf = append(buf, clientPublic[:]...)
buf = append(buf, []byte(payloadM3.Identifier)...)
buf = append(buf, sessionPublic[:]...)
if !ed25519.ValidateSignature(
pairing[:32], buf, payloadM3.Signature,
) {
err = errors.New("signature invalid")
return
}
}
// STEP M4. Response to iPhone
payloadM4 := struct {
State byte `tlv8:"6"`
}{
State: hap.M4,
}
if buf, err = tlv8.Marshal(payloadM4); err != nil {
return
}
err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf)
if secure, err = NewSecure(sessionShared, true); err != nil {
return
}
secure.Conn = conn
return
}
+137
View File
@@ -0,0 +1,137 @@
package hap
import (
"encoding/binary"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/hkdf"
"net"
"sync"
)
type Secure struct {
Conn net.Conn
encryptKey [32]byte
decryptKey [32]byte
encryptCount uint64
decryptCount uint64
mx sync.Mutex
}
func NewSecure(sharedKey [32]byte, isServer bool) (*Secure, error) {
salt := []byte("Control-Salt")
key1, err := hkdf.Sha512(
sharedKey[:], salt, []byte("Control-Read-Encryption-Key"),
)
if err != nil {
return nil, err
}
key2, err := hkdf.Sha512(
sharedKey[:], salt, []byte("Control-Write-Encryption-Key"),
)
if err != nil {
return nil, err
}
if isServer {
return &Secure{encryptKey: key1, decryptKey: key2}, nil
} else {
return &Secure{encryptKey: key2, decryptKey: key1}, nil
}
}
func (s *Secure) Read(b []byte) (n int, err error) {
for {
var length uint16
if err = binary.Read(s.Conn, binary.LittleEndian, &length); err != nil {
return
}
var enc = make([]byte, length)
if err = binary.Read(s.Conn, binary.LittleEndian, &enc); err != nil {
return
}
var mac [16]byte
if err = binary.Read(s.Conn, binary.LittleEndian, &mac); err != nil {
return
}
var nonce [8]byte
binary.LittleEndian.PutUint64(nonce[:], s.decryptCount)
s.decryptCount++
bLength := make([]byte, 2)
binary.LittleEndian.PutUint16(bLength, length)
var msg []byte
if msg, err = chacha20poly1305.DecryptAndVerify(
s.decryptKey[:], nonce[:], enc, mac, bLength,
); err != nil {
return
}
n += copy(b[n:], msg)
// Finish when all bytes fit in b
if length < packetLengthMax {
//fmt.Printf(">>>%s>>>\n", b[:n])
return
}
}
}
func (s *Secure) Write(b []byte) (n int, err error) {
s.mx.Lock()
defer s.mx.Unlock()
var packetLen = len(b)
for {
if packetLen > packetLengthMax {
packetLen = packetLengthMax
}
//fmt.Printf("<<<%s<<<\n", b[:packetLen])
var nonce [8]byte
binary.LittleEndian.PutUint64(nonce[:], s.encryptCount)
s.encryptCount++
bLength := make([]byte, 2)
binary.LittleEndian.PutUint16(bLength, uint16(packetLen))
var enc []byte
var mac [16]byte
enc, mac, err = chacha20poly1305.EncryptAndSeal(
s.encryptKey[:], nonce[:], b[:packetLen], bLength[:],
)
if err != nil {
return
}
enc = append(bLength, enc...)
enc = append(enc, mac[:]...)
if _, err = s.Conn.Write(enc); err != nil {
return
}
n += packetLen
if packetLen == packetLengthMax {
b = b[packetLengthMax:]
packetLen = len(b)
} else {
break
}
}
return
}
const (
// packetLengthMax is the max length of encrypted packets
packetLengthMax = 0x400
)
+155
View File
@@ -0,0 +1,155 @@
package hap
import (
"bufio"
"crypto/ed25519"
"github.com/brutella/hap"
"github.com/brutella/hap/tlv8"
"io"
"net"
"net/http"
)
type Server struct {
// Pin can't be null because server proof will be wrong
Pin string `json:"-"`
ServerID string `json:"server_id"`
// 32 bytes private key + 32 bytes public key
ServerPrivate []byte `json:"server_private"`
// Pairings can be nil for disable pair verify check
// ClientID: 32 bytes client public + 1 byte (isAdmin)
Pairings map[string][]byte `json:"pairings"`
DefaultPlainHandler func(w io.Writer, r *http.Request) error
DefaultSecureHandler func(w io.Writer, r *http.Request) error
OnPairChange func(clientID string, clientPublic []byte) `json:"-"`
OnRequest func(w io.Writer, r *http.Request) `json:"-"`
}
func GenerateKey() []byte {
_, key, _ := ed25519.GenerateKey(nil)
return key
}
func NewServer(name string) *Server {
return &Server{
ServerID: GenerateID(name),
ServerPrivate: GenerateKey(),
Pairings: map[string][]byte{},
}
}
func (s *Server) Serve(address string) (err error) {
var ln net.Listener
if ln, err = net.Listen("tcp", address); err != nil {
return
}
for {
var conn net.Conn
if conn, err = ln.Accept(); err != nil {
continue
}
go func() {
//fmt.Printf("[%s] new connection\n", conn.RemoteAddr().String())
s.Accept(conn)
//fmt.Printf("[%s] close connection\n", conn.RemoteAddr().String())
}()
}
}
func (s *Server) Accept(conn net.Conn) (err error) {
defer conn.Close()
var req *http.Request
r := bufio.NewReader(conn)
if req, err = http.ReadRequest(r); err != nil {
return
}
return s.HandleRequest(conn, req)
}
func (s *Server) HandleRequest(conn net.Conn, req *http.Request) (err error) {
if s.OnRequest != nil {
s.OnRequest(conn, req)
}
switch req.URL.Path {
case UriPairSetup:
if _, err = s.PairSetupHandler(conn, req); err != nil {
return
}
case UriPairVerify:
var secure *Secure
if secure, err = s.PairVerifyHandler(conn, req); err != nil {
return
}
err = s.HandleSecure(secure)
default:
if s.DefaultPlainHandler != nil {
err = s.DefaultPlainHandler(conn, req)
}
}
return
}
func (s *Server) HandleSecure(secure *Secure) (err error) {
r := bufio.NewReader(secure)
for {
var req *http.Request
if req, err = http.ReadRequest(r); err != nil {
return
}
if s.OnRequest != nil {
s.OnRequest(secure, req)
}
switch req.URL.Path {
case UriPairings:
s.HandlePairings(secure, req)
default:
if err = s.DefaultSecureHandler(secure, req); err != nil {
return
}
}
}
}
func (s *Server) HandlePairings(w io.Writer, r *http.Request) {
req := struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Permission byte `tlv8:"11"`
State byte `tlv8:"6"`
}{}
if err := tlv8.UnmarshalReader(r.Body, &req); err != nil {
panic(err)
}
switch req.Method {
case hap.MethodAddPairing, hap.MethodDeletePairing:
res := struct {
State byte `tlv8:"6"`
}{
State: hap.M2,
}
data, err := tlv8.Marshal(res)
if err != nil {
panic(err)
}
if err = WriteResponse(w, http.StatusOK, MimeJSON, data); err != nil {
panic(err)
}
}
}