Refactoring for HomeKit client
This commit is contained in:
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user