Rewrite HomeKit client

This commit is contained in:
Alexey Khit
2023-08-30 21:52:06 +03:00
parent 7d65c60711
commit 22787b979d
30 changed files with 1094 additions and 916 deletions
+8
View File
@@ -35,6 +35,14 @@ Requires ffmpeg built with `--enable-libfdk-aac`
-acodec libfdk_aac -aprofile aac_eld
```
| SampleRate | RTPTime | constantDuration | objectType |
|------------|---------|--------------------|--------------|
| 8000 | 60 | =8000/1000*60=480 | 39 (AAC ELD) |
| 16000 | 30 | =16000/1000*30=480 | 39 (AAC ELD) |
| 24000 | 20 | =24000/1000*20=480 | 39 (AAC ELD) |
| 16000 | 60 | =16000/1000*60=960 | 23 (AAC LD) |
| 24000 | 40 | =24000/1000*40=960 | 23 (AAC LD) |
## Useful links
- https://github.com/apple/HomeKitADK/blob/master/Documentation/crypto.md
+124 -12
View File
@@ -1,16 +1,64 @@
package hap
import (
"fmt"
"strconv"
)
const (
FormatString = "string"
FormatBool = "bool"
FormatFloat = "float"
FormatUInt8 = "uint8"
FormatUInt16 = "uint16"
FormatUInt32 = "uint32"
FormatInt32 = "int32"
FormatUInt64 = "uint64"
FormatData = "data"
FormatTLV8 = "tlv8"
UnitPercentage = "percentage"
)
var PR = []string{"pr"}
var PW = []string{"pw"}
var PRPW = []string{"pr", "pw"}
var EVPRPW = []string{"ev", "pr", "pw"}
var EVPR = []string{"ev", "pr"}
type Accessory struct {
AID int `json:"aid"`
AID uint8 `json:"aid"` // 150 unique accessories per bridge
Services []*Service `json:"services"`
}
type Accessories struct {
Accessories []*Accessory `json:"accessories"`
}
func (a *Accessory) InitIID() {
serviceN := map[string]byte{}
for _, service := range a.Services {
if len(service.Type) > 3 {
panic(service.Type)
}
type Characters struct {
Characters []*Character `json:"characteristics"`
n := serviceN[service.Type] + 1
serviceN[service.Type] = n
if n > 15 {
panic(n)
}
// ServiceID = ANSSS000
s := fmt.Sprintf("%x%x%03s000", a.AID, n, service.Type)
service.IID, _ = strconv.ParseUint(s, 16, 64)
for _, character := range service.Characters {
if len(character.Type) > 3 {
panic(character.Type)
}
// CharacterID = ANSSSCCC
character.IID, _ = strconv.ParseUint(character.Type, 16, 64)
character.IID += service.IID
}
}
}
func (a *Accessory) GetService(servType string) *Service {
@@ -33,7 +81,7 @@ func (a *Accessory) GetCharacter(charType string) *Character {
return nil
}
func (a *Accessory) GetCharacterByID(iid int) *Character {
func (a *Accessory) GetCharacterByID(iid uint64) *Character {
for _, serv := range a.Services {
for _, char := range serv.Characters {
if char.IID == iid {
@@ -45,11 +93,11 @@ func (a *Accessory) GetCharacterByID(iid int) *Character {
}
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"`
Type string `json:"type"`
IID uint64 `json:"iid"`
Primary bool `json:"primary,omitempty"`
Characters []*Character `json:"characteristics"`
Linked []int `json:"linked,omitempty"`
}
func (s *Service) GetCharacter(charType string) *Character {
@@ -60,3 +108,67 @@ func (s *Service) GetCharacter(charType string) *Character {
}
return nil
}
func ServiceAccessoryInformation(manuf, model, name, serial, firmware string) *Service {
return &Service{
Type: "3E", // AccessoryInformation
Characters: []*Character{
{
Type: "14",
Format: FormatBool,
Perms: PW,
//Descr: "Identify",
}, {
Type: "20",
Format: FormatString,
Value: manuf,
Perms: PR,
//Descr: "Manufacturer",
//MaxLen: 64,
}, {
Type: "21",
Format: FormatString,
Value: model,
Perms: PR,
//Descr: "Model",
//MaxLen: 64,
}, {
Type: "23",
Format: FormatString,
Value: name,
Perms: PR,
//Descr: "Name",
//MaxLen: 64,
}, {
Type: "30",
Format: FormatString,
Value: serial,
Perms: PR,
//Descr: "Serial Number",
//MaxLen: 64,
}, {
Type: "52",
Format: FormatString,
Value: firmware,
Perms: PR,
//Descr: "Firmware Revision",
},
},
}
}
func ServiceHAPProtocolInformation() *Service {
return &Service{
Type: "A2", // 'HAPProtocolInformation'
Characters: []*Character{
{
Type: "37",
Format: FormatString,
Value: "1.1.0",
Perms: PR,
//Descr: "Version",
//MaxLen: 64,
},
},
}
}
+13 -11
View File
@@ -3,15 +3,17 @@ package camera
const TypeSupportedVideoStreamConfiguration = "114"
type SupportedVideoStreamConfig struct {
Codecs []VideoCodecConfig `tlv8:"1"`
Codecs []VideoCodec `tlv8:"1"`
}
type VideoCodecConfig struct {
CodecType byte `tlv8:"1"`
CodecParams []VideoCodecParams `tlv8:"2"`
VideoAttrs []VideoAttrs `tlv8:"3"`
type VideoCodec struct {
CodecType byte `tlv8:"1"`
CodecParams []VideoParams `tlv8:"2"`
VideoAttrs []VideoAttrs `tlv8:"3"`
RTPParams []RTPParams `tlv8:"4"`
}
//goland:noinspection ALL
const (
VideoCodecTypeH264 = 0
@@ -29,12 +31,12 @@ const (
VideoCodecCvoSuppported = 1
)
type VideoCodecParams struct {
ProfileID byte `tlv8:"1"` // 0 - baseline, 1 - main, 2 - high
Level byte `tlv8:"2"` // 0 - 3.1, 1 - 3.2, 2 - 4.0
PacketizationMode byte `tlv8:"3"` // only 0 - non interleaved
CVOEnabled byte `tlv8:"4"` // 0 - not supported, 1 - supported
CVOID byte `tlv8:"5"` // ???
type VideoParams struct {
ProfileID []byte `tlv8:"1"` // 0 - baseline, 1 - main, 2 - high
Level []byte `tlv8:"2"` // 0 - 3.1, 1 - 3.2, 2 - 4.0
PacketizationMode byte `tlv8:"3"` // only 0 - non interleaved
CVOEnabled []byte `tlv8:"4"` // 0 - not supported, 1 - supported
CVOID []byte `tlv8:"5"` // ???
}
type VideoAttrs struct {
+19 -10
View File
@@ -3,10 +3,11 @@ package camera
const TypeSupportedAudioStreamConfiguration = "115"
type SupportedAudioStreamConfig struct {
Codecs []AudioCodecConfig `tlv8:"1"`
ComfortNoise byte `tlv8:"2"`
Codecs []AudioCodec `tlv8:"1"`
ComfortNoise byte `tlv8:"2"`
}
//goland:noinspection ALL
const (
AudioCodecTypePCMU = 0
AudioCodecTypePCMA = 1
@@ -22,16 +23,24 @@ const (
AudioCodecSampleRate8Khz = 0
AudioCodecSampleRate16Khz = 1
AudioCodecSampleRate24Khz = 2
RTPTimeAACELD8 = 60 // 8000/1000*60=480
RTPTimeAACELD16 = 30 // 16000/1000*30=480
RTPTimeAACELD24 = 20 // 24000/1000*20=480
RTPTimeAACLD16 = 60 // 16000/1000*60=960
RTPTimeAACLD24 = 40 // 24000/1000*40=960
)
type AudioCodecConfig struct {
CodecType byte `tlv8:"1"`
CodecParams []AudioCodecParams `tlv8:"2"`
type AudioCodec struct {
CodecType byte `tlv8:"1"`
CodecParams []AudioParams `tlv8:"2"`
RTPParams []RTPParams `tlv8:"3"`
ComfortNoise []byte `tlv8:"4"`
}
type AudioCodecParams struct {
Channels byte `tlv8:"1"`
Bitrate byte `tlv8:"2"` // 0 - variable, 1 - constant
SampleRate byte `tlv8:"3"` // 0 - 8000, 1 - 16000, 2 - 24000
RTPTime byte `tlv8:"4"`
type AudioParams struct {
Channels uint8 `tlv8:"1"`
Bitrate byte `tlv8:"2"` // 0 - variable, 1 - constant
SampleRate []byte `tlv8:"3"` // 0 - 8000, 1 - 16000, 2 - 24000
RTPTime []uint8 `tlv8:"4"` // 20, 30, 40, 60
}
+14
View File
@@ -0,0 +1,14 @@
package camera
const TypeSupportedRTPConfiguration = "116"
//goland:noinspection ALL
const (
CryptoAES_CM_128_HMAC_SHA1_80 = 0
CryptoAES_CM_256_HMAC_SHA1_80 = 1
CryptoNone = 2
)
type SupportedRTPConfig struct {
CryptoType []byte `tlv8:"2"`
}
+13 -33
View File
@@ -3,11 +3,12 @@ package camera
const TypeSelectedStreamConfiguration = "117"
type SelectedStreamConfig struct {
Control SessionControl `tlv8:"1"`
VideoParams SelectedVideoParams `tlv8:"2"`
AudioParams SelectedAudioParams `tlv8:"3"`
Control SessionControl `tlv8:"1"`
VideoCodec VideoCodec `tlv8:"2"`
AudioCodec AudioCodec `tlv8:"3"`
}
//goland:noinspection ALL
const (
SessionCommandEnd = 0
SessionCommandStart = 1
@@ -17,36 +18,15 @@ const (
)
type SessionControl struct {
Session string `tlv8:"1"`
Command byte `tlv8:"2"`
SessionID string `tlv8:"1"`
Command byte `tlv8:"2"`
}
type SelectedVideoParams struct {
CodecType byte `tlv8:"1"` // only 0 - H264
CodecParams VideoCodecParams `tlv8:"2"`
VideoAttrs VideoAttrs `tlv8:"3"`
RTPParams VideoRTPParams `tlv8:"4"`
}
type VideoRTPParams struct {
PayloadType uint8 `tlv8:"1"`
SSRC uint32 `tlv8:"2"`
MaxBitrate uint16 `tlv8:"3"`
MinRTCPInterval float32 `tlv8:"4"`
MaxMTU uint16 `tlv8:"5"`
}
type SelectedAudioParams struct {
CodecType byte `tlv8:"1"` // 2 - AAC_ELD, 3 - OPUS, 5 - AMR, 6 - AMR_WB
CodecParams AudioCodecParams `tlv8:"2"`
RTPParams AudioRTPParams `tlv8:"3"`
ComfortNoise uint8 `tlv8:"4"`
}
type AudioRTPParams struct {
PayloadType uint8 `tlv8:"1"`
SSRC uint32 `tlv8:"2"`
MaxBitrate uint16 `tlv8:"3"`
MinRTCPInterval float32 `tlv8:"4"`
ComfortNoisePayloadType uint8 `tlv8:"6"`
type RTPParams struct {
PayloadType uint8 `tlv8:"1"`
SSRC uint32 `tlv8:"2"`
MaxBitrate uint16 `tlv8:"3"`
MinRTCPInterval float32 `tlv8:"4"`
MaxMTU []uint16 `tlv8:"5"`
ComfortNoisePayloadType []uint8 `tlv8:"6"`
}
+9 -16
View File
@@ -3,10 +3,13 @@ package camera
const TypeSetupEndpoints = "118"
type SetupEndpoints struct {
SessionID []byte `tlv8:"1"`
ControllerAddr Addr `tlv8:"3"`
VideoCrypto CryptoSuite `tlv8:"4"`
AudioCrypto CryptoSuite `tlv8:"5"`
SessionID string `tlv8:"1"`
Status []byte `tlv8:"2"`
Address Addr `tlv8:"3"`
VideoCrypto CryptoSuite `tlv8:"4"`
AudioCrypto CryptoSuite `tlv8:"5"`
VideoSSRC []uint32 `tlv8:"6"`
AudioSSRC []uint32 `tlv8:"7"`
}
type Addr struct {
@@ -18,16 +21,6 @@ type Addr struct {
type CryptoSuite struct {
CryptoType byte `tlv8:"1"`
MasterKey []byte `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM)
MasterSalt []byte `tlv8:"3"` // 14 byte
}
type SetupEndpointsResponse struct {
SessionID []byte `tlv8:"1"`
Status byte `tlv8:"2"`
AccessoryAddr Addr `tlv8:"3"`
VideoCrypto CryptoSuite `tlv8:"4"`
AudioCrypto CryptoSuite `tlv8:"5"`
VideoSSRC uint32 `tlv8:"6"`
AudioSSRC uint32 `tlv8:"7"`
MasterKey string `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM)
MasterSalt string `tlv8:"3"` // 14 byte
}
+1
View File
@@ -6,6 +6,7 @@ type StreamingStatus struct {
Status byte `tlv8:"1"`
}
//goland:noinspection ALL
const (
StreamingStatusAvailable = 0
StreamingStatusBusy = 1
-97
View File
@@ -1,97 +0,0 @@
package camera
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/hap"
)
type Client struct {
client *hap.Client
}
func NewClient(client *hap.Client) *Client {
return &Client{client: client}
}
func (c *Client) StartStream(ses *Session) error {
// Step 1. Check if camera ready (free) to stream
srv, err := c.GetFreeStream()
if 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 err
}
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) {
accs, err := c.client.GetAccessories()
if err != nil {
return
}
for _, srv = range accs[0].Services {
for _, char := range srv.Characters {
if char.Type == TypeStreamingStatus {
var status StreamingStatus
if err = char.ReadTLV8(&status); err != nil {
return
}
if status.Status == StreamingStatusAvailable {
return
}
}
}
}
return nil, nil
}
func (c *Client) SetupEndpoins(srv *hap.Service, req *SetupEndpoints) (res *SetupEndpointsResponse, err error) {
// get setup endpoint character ID
char := srv.GetCharacter(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 = &SetupEndpointsResponse{}
if err = char.ReadTLV8(res); err != nil {
return
}
return
}
func (c *Client) SetConfig(srv *hap.Service, config *SelectedStreamConfig) error {
// get setup endpoint character ID
char := srv.GetCharacter(TypeSelectedStreamConfiguration)
char.Event = nil
// encode new character value
if err := char.Write(config); err != nil {
return err
}
// write (put) new character value to device
return c.client.PutCharacters(char)
}
-73
View File
@@ -1,73 +0,0 @@
package camera
import (
"crypto/rand"
"encoding/binary"
)
type Session struct {
Offer *SetupEndpoints
Answer *SetupEndpointsResponse
Config *SelectedStreamConfig
}
func NewSession(vp *SelectedVideoParams, ap *SelectedAudioParams) *Session {
vp.RTPParams = VideoRTPParams{
PayloadType: 99,
SSRC: RandomUint32(),
MaxBitrate: 2048,
MinRTCPInterval: 10,
MaxMTU: 1200, // like WebRTC
}
ap.RTPParams = AudioRTPParams{
PayloadType: 110,
SSRC: RandomUint32(),
MaxBitrate: 32,
MinRTCPInterval: 10,
ComfortNoisePayloadType: 98,
}
sessionID := RandomBytes(16)
s := &Session{
Offer: &SetupEndpoints{
SessionID: sessionID,
VideoCrypto: CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
AudioCrypto: CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
},
Config: &SelectedStreamConfig{
Control: SessionControl{
Session: string(sessionID),
Command: SessionCommandStart,
},
VideoParams: *vp,
AudioParams: *ap,
},
}
return s
}
func (s *Session) SetLocalEndpoint(host string, port uint16) {
s.Offer.ControllerAddr = Addr{
IPAddr: host,
VideoRTPPort: port,
AudioRTPPort: port,
}
}
func RandomBytes(size int) []byte {
data := make([]byte, size)
_, _ = rand.Read(data)
return data
}
func RandomUint32() uint32 {
data := make([]byte, 4)
_, _ = rand.Read(data)
return binary.BigEndian.Uint32(data)
}
+177
View File
@@ -0,0 +1,177 @@
package camera
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/srtp"
)
type Stream struct {
id string
client *hap.Client
service *hap.Service
}
func NewStream(
client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec, videoSession, audioSession *srtp.Session,
) (*Stream, error) {
stream := &Stream{
id: core.RandString(16, 0),
client: client,
}
if err := stream.GetFreeStream(); err != nil {
return nil, err
}
if err := stream.ExchangeEndpoints(videoSession, audioSession); err != nil {
return nil, err
}
videoCodec.RTPParams = []RTPParams{
{
PayloadType: 99,
SSRC: videoSession.Local.SSRC,
MaxBitrate: 299,
MinRTCPInterval: 0.5,
MaxMTU: []uint16{1378},
},
}
audioCodec.RTPParams = []RTPParams{
{
PayloadType: 110,
SSRC: audioSession.Local.SSRC,
MaxBitrate: 24,
MinRTCPInterval: 5,
ComfortNoisePayloadType: []uint8{13},
},
}
audioCodec.ComfortNoise = []byte{0}
config := &SelectedStreamConfig{
Control: SessionControl{
SessionID: stream.id,
Command: SessionCommandStart,
},
VideoCodec: *videoCodec,
AudioCodec: *audioCodec,
}
if err := stream.SetStreamConfig(config); err != nil {
return nil, err
}
return stream, nil
}
// 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 (s *Stream) GetFreeStream() error {
acc, err := s.client.GetFirstAccessory()
if err != nil {
return err
}
for _, srv := range acc.Services {
for _, char := range srv.Characters {
if char.Type == TypeStreamingStatus {
var status StreamingStatus
if err = char.ReadTLV8(&status); err != nil {
return err
}
if status.Status == StreamingStatusAvailable {
s.service = srv
return nil
}
}
}
}
return errors.New("hap: no free streams")
}
func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) error {
req := SetupEndpoints{
SessionID: s.id,
Address: Addr{
IPVersion: 0,
IPAddr: videoSession.Local.Addr,
VideoRTPPort: videoSession.Local.Port,
AudioRTPPort: audioSession.Local.Port,
},
VideoCrypto: CryptoSuite{
MasterKey: string(videoSession.Local.MasterKey),
MasterSalt: string(videoSession.Local.MasterSalt),
},
AudioCrypto: CryptoSuite{
MasterKey: string(audioSession.Local.MasterKey),
MasterSalt: string(audioSession.Local.MasterSalt),
},
}
char := s.service.GetCharacter(TypeSetupEndpoints)
if err := char.Write(&req); err != nil {
return err
}
if err := s.client.PutCharacters(char); err != nil {
return err
}
var res SetupEndpoints
if err := s.client.GetCharacter(char); err != nil {
return err
}
if err := char.ReadTLV8(&res); err != nil {
return err
}
videoSession.Remote = &srtp.Endpoint{
Addr: res.Address.IPAddr,
Port: res.Address.VideoRTPPort,
MasterKey: []byte(res.VideoCrypto.MasterKey),
MasterSalt: []byte(res.VideoCrypto.MasterSalt),
SSRC: res.VideoSSRC[0],
}
audioSession.Remote = &srtp.Endpoint{
Addr: res.Address.IPAddr,
Port: res.Address.AudioRTPPort,
MasterKey: []byte(res.AudioCrypto.MasterKey),
MasterSalt: []byte(res.AudioCrypto.MasterSalt),
SSRC: res.AudioSSRC[0],
}
return nil
}
func (s *Stream) SetStreamConfig(config *SelectedStreamConfig) error {
char := s.service.GetCharacter(TypeSelectedStreamConfiguration)
if err := char.Write(config); err != nil {
return err
}
if err := s.client.PutCharacters(char); err != nil {
return err
}
return s.client.GetCharacter(char)
}
func (s *Stream) Close() error {
config := &SelectedStreamConfig{
Control: SessionControl{
SessionID: s.id,
Command: SessionCommandEnd,
},
}
char := s.service.GetCharacter(TypeSelectedStreamConfiguration)
if err := char.Write(config); err != nil {
return err
}
return s.client.PutCharacters(char)
}
+21 -12
View File
@@ -9,16 +9,23 @@ import (
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
)
// Character - Aqara props order
// Value should be omit for PW
// Value may be empty for PR
type Character struct {
AID int `json:"aid,omitempty"`
IID int `json:"iid"`
Type string `json:"type,omitempty"`
Format string `json:"format,omitempty"`
Value any `json:"value,omitempty"`
Event any `json:"ev,omitempty"`
Perms []string `json:"perms,omitempty"`
Description string `json:"description,omitempty"`
//MaxDataLen int `json:"maxDataLen"`
IID uint64 `json:"iid"`
Type string `json:"type"`
Format string `json:"format"`
Value any `json:"value,omitempty"`
Perms []string `json:"perms"`
//Descr string `json:"description,omitempty"`
//MaxLen int `json:"maxLen,omitempty"`
//Unit string `json:"unit,omitempty"`
//MinValue any `json:"minValue,omitempty"`
//MaxValue any `json:"maxValue,omitempty"`
//MinStep any `json:"minStep,omitempty"`
//ValidVal []any `json:"valid-values,omitempty"`
listeners map[io.Writer]bool
}
@@ -64,10 +71,12 @@ func (c *Character) NotifyListeners(ignore io.Writer) error {
// 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}},
v := JSONCharacters{
Value: []JSONCharacter{
{AID: DeviceAID, IID: c.IID, Value: c.Value},
},
}
if data, err = json.Marshal(chars); err != nil {
if data, err = json.Marshal(v); err != nil {
return
}
+53 -48
View File
@@ -37,9 +37,9 @@ type Client struct {
ClientPrivate []byte
OnEvent func(res *http.Response)
Output func(msg any)
//Output func(msg any)
conn net.Conn
Conn net.Conn
reader *bufio.Reader
}
@@ -89,21 +89,21 @@ func (c *Client) Dial() (err error) {
return false
})
if c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout); err != nil {
if c.Conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout); err != nil {
return
}
c.reader = bufio.NewReader(c.conn)
c.reader = bufio.NewReader(c.Conn)
// STEP M1: send our session public to device
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
// 1. Send sessionPublic
plainM1 := struct {
PublicKey []byte `tlv8:"3"`
PublicKey string `tlv8:"3"`
State byte `tlv8:"6"`
}{
PublicKey: sessionPublic,
PublicKey: string(sessionPublic),
State: StateM1,
}
res, err := c.Post(PathPairVerify, MimeTLV8, tlv8.MarshalReader(plainM1))
@@ -113,19 +113,19 @@ func (c *Client) Dial() (err error) {
// STEP M2: unpack deviceID from response
var cipherM2 struct {
PublicKey []byte `tlv8:"3"`
EncryptedData []byte `tlv8:"5"`
PublicKey string `tlv8:"3"`
EncryptedData string `tlv8:"5"`
State byte `tlv8:"6"`
}
if err = tlv8.UnmarshalReader(res.Body, &cipherM2); err != nil {
return err
}
if cipherM2.State != StateM2 {
return NewResponseError(plainM1, cipherM2)
return newResponseError(plainM1, cipherM2)
}
// 1. generate session shared key
sessionShared, err := curve25519.SharedSecret(sessionPrivate, cipherM2.PublicKey)
sessionShared, err := curve25519.SharedSecret(sessionPrivate, []byte(cipherM2.PublicKey))
if err != nil {
return
}
@@ -138,7 +138,7 @@ func (c *Client) Dial() (err error) {
}
// 2. decrypt M2 response with session key
b, err := chacha20poly1305.Decrypt(sessionKey, "PV-Msg02", cipherM2.EncryptedData)
b, err := chacha20poly1305.Decrypt(sessionKey, "PV-Msg02", []byte(cipherM2.EncryptedData))
if err != nil {
return
}
@@ -146,7 +146,7 @@ func (c *Client) Dial() (err error) {
// 3. unpack payload from TLV8
var plainM2 struct {
Identifier string `tlv8:"1"`
Signature []byte `tlv8:"10"`
Signature string `tlv8:"10"`
}
if err = tlv8.Unmarshal(b, &plainM2); err != nil {
return
@@ -156,7 +156,7 @@ func (c *Client) Dial() (err error) {
// device session + device id + our session
if c.DevicePublic != nil {
b = Append(cipherM2.PublicKey, plainM2.Identifier, sessionPublic)
if !ed25519.ValidateSignature(c.DevicePublic, b, plainM2.Signature) {
if !ed25519.ValidateSignature(c.DevicePublic, b, []byte(plainM2.Signature)) {
return errors.New("hap: ValidateSignature")
}
}
@@ -172,10 +172,10 @@ func (c *Client) Dial() (err error) {
// 2. generate payload
plainM3 := struct {
Identifier string `tlv8:"1"`
Signature []byte `tlv8:"10"`
Signature string `tlv8:"10"`
}{
Identifier: c.ClientID,
Signature: b,
Signature: string(b),
}
if b, err = tlv8.Marshal(plainM3); err != nil {
return
@@ -188,11 +188,11 @@ func (c *Client) Dial() (err error) {
// 4. generate request
cipherM3 := struct {
EncryptedData []byte `tlv8:"5"`
EncryptedData string `tlv8:"5"`
State byte `tlv8:"6"`
}{
State: StateM3,
EncryptedData: b,
EncryptedData: string(b),
}
if res, err = c.Post(PathPairVerify, MimeTLV8, tlv8.MarshalReader(cipherM3)); err != nil {
return
@@ -206,25 +206,25 @@ func (c *Client) Dial() (err error) {
return
}
if plainM4.State != StateM4 {
return NewResponseError(cipherM3, plainM4)
return newResponseError(cipherM3, plainM4)
}
// like tls.Client wrapper over net.Conn
if c.conn, err = secure.Client(c.conn, sessionShared, true); err != nil {
if c.Conn, err = secure.Client(c.Conn, sessionShared, true); err != nil {
return
}
// new reader for new conn
c.reader = bufio.NewReaderSize(c.conn, 32*1024) // 32K like default request body
c.reader = bufio.NewReaderSize(c.Conn, 32*1024) // 32K like default request body
return
}
func (c *Client) Close() error {
if c.conn == nil {
if c.Conn == nil {
return nil
}
conn := c.conn
c.conn = nil
conn := c.Conn
c.Conn = nil
return conn.Close()
}
@@ -234,23 +234,26 @@ func (c *Client) GetAccessories() ([]*Accessory, error) {
return nil, err
}
var ac Accessories
if err = json.NewDecoder(res.Body).Decode(&ac); err != nil {
var v JSONAccessories
if err = json.NewDecoder(res.Body).Decode(&v); err != nil {
return nil, err
}
for _, accs := range ac.Accessories {
for _, serv := range accs.Services {
for _, char := range serv.Characters {
char.AID = accs.AID
}
}
}
return ac.Accessories, nil
return v.Value, nil
}
func (c *Client) GetCharacters(query string) ([]*Character, error) {
func (c *Client) GetFirstAccessory() (*Accessory, error) {
accs, err := c.GetAccessories()
if err != nil {
return nil, err
}
if len(accs) == 0 {
return nil, errors.New("hap: GetAccessories zero answer")
}
return accs[0], nil
}
func (c *Client) GetCharacters(query string) ([]JSONCharacter, error) {
res, err := c.Get(PathCharacteristics + "?id=" + query)
if err != nil {
return nil, err
@@ -261,15 +264,15 @@ func (c *Client) GetCharacters(query string) ([]*Character, error) {
return nil, err
}
var ch Characters
if err = json.Unmarshal(data, &ch); err != nil {
var v JSONCharacters
if err = json.Unmarshal(data, &v); err != nil {
return nil, err
}
return ch.Characters, nil
return v.Value, nil
}
func (c *Client) GetCharacter(char *Character) error {
query := fmt.Sprintf("%d.%d", char.AID, char.IID)
query := fmt.Sprintf("%d.%d", DeviceAID, char.IID)
chars, err := c.GetCharacters(query)
if err != nil {
return err
@@ -279,20 +282,21 @@ func (c *Client) GetCharacter(char *Character) error {
}
func (c *Client) PutCharacters(characters ...*Character) error {
var v JSONCharacters
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}
}
v.Value = append(v.Value, JSONCharacter{
AID: 1,
IID: char.IID,
Value: char.Value,
})
characters[i] = char
}
data, err := json.Marshal(Characters{characters})
body, err := json.Marshal(v)
if err != nil {
return err
}
_, err = c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(data))
_, err = c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(body))
if err != nil {
return err
}
@@ -312,8 +316,9 @@ func (c *Client) GetImage(width, height int) ([]byte, error) {
return io.ReadAll(res.Body)
}
func (c *Client) LocalAddr() string {
return c.conn.LocalAddr().String()
func (c *Client) LocalIP() string {
addr := c.Conn.LocalAddr().(*net.TCPAddr)
return addr.IP.To4().String()
}
func DecodeKey(s string) []byte {
+1 -5
View File
@@ -4,7 +4,6 @@ import (
"errors"
"io"
"net/http"
"time"
)
const (
@@ -20,10 +19,7 @@ const (
)
func (c *Client) Do(req *http.Request) (*http.Response, error) {
if err := c.conn.SetWriteDeadline(time.Now().Add(ConnDeadline)); err != nil {
return nil, err
}
if err := req.Write(c.conn); err != nil {
if err := req.Write(c.Conn); err != nil {
return nil, err
}
return http.ReadResponse(c.reader, req)
+36 -41
View File
@@ -4,9 +4,7 @@ import (
"bufio"
"crypto/sha512"
"errors"
"fmt"
"net"
"strings"
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
"github.com/AlexxIT/go2rtc/pkg/hap/ed25519"
@@ -21,9 +19,9 @@ func Pair(deviceID, pin string) (*Client, error) {
var mfi bool
_ = mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
if entry.Complete() && entry.Info["id"] == deviceID {
if entry.Complete() && entry.Info[TXTDeviceID] == deviceID {
addr = entry.Addr()
mfi = entry.Info["ff"] == "1"
mfi = entry.Info[TXTFeatureFlags] == "1"
return true
}
return false
@@ -44,19 +42,16 @@ func Pair(deviceID, pin string) (*Client, error) {
}
func (c *Client) Pair(mfi bool, pin string) (err error) {
pin = strings.ReplaceAll(pin, "-", "")
if len(pin) != 8 {
return fmt.Errorf("wrong PIN format: %s", pin)
if pin, err = SanitizePin(pin); err != nil {
return err
}
pin = pin[:3] + "-" + pin[3:5] + "-" + pin[5:] // 123-45-678
c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout)
c.Conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout)
if err != nil {
return
}
c.reader = bufio.NewReader(c.conn)
c.reader = bufio.NewReader(c.Conn)
// STEP M1. Send HELLO
plainM1 := struct {
@@ -76,8 +71,8 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
// STEP M2. Read Device Salt and session PublicKey
var plainM2 struct {
Salt []byte `tlv8:"2"`
SessionKey []byte `tlv8:"3"` // server public key, aka session.B
Salt string `tlv8:"2"`
SessionKey string `tlv8:"3"` // server public key, aka session.B
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}
@@ -85,7 +80,7 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
return
}
if plainM2.State != StateM2 {
return NewResponseError(plainM1, plainM2)
return newResponseError(plainM1, plainM2)
}
if plainM2.Error != 0 {
return newPairingError(plainM2.Error)
@@ -106,19 +101,19 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
// username: "Pair-Setup", password: PIN (with dashes)
session := pake.NewClientSession(username, []byte(pin))
sessionShared, err := session.ComputeKey(plainM2.Salt, plainM2.SessionKey)
sessionShared, err := session.ComputeKey([]byte(plainM2.Salt), []byte(plainM2.SessionKey))
if err != nil {
return
}
// STEP M3. Send request
plainM3 := struct {
SessionKey []byte `tlv8:"3"`
Proof []byte `tlv8:"4"`
SessionKey string `tlv8:"3"`
Proof string `tlv8:"4"`
State byte `tlv8:"6"`
}{
SessionKey: session.GetA(), // client public key, aka session.A
Proof: session.ComputeAuthenticator(),
SessionKey: string(session.GetA()), // client public key, aka session.A
Proof: string(session.ComputeAuthenticator()),
State: StateM3,
}
if res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(plainM3)); err != nil {
@@ -127,7 +122,7 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
// STEP M4. Read response
var plainM4 struct {
Proof []byte `tlv8:"4"` // server proof
Proof string `tlv8:"4"` // server proof
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}
@@ -135,15 +130,15 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
return
}
if plainM4.State != StateM4 {
return NewResponseError(plainM3, plainM4)
return newResponseError(plainM3, plainM4)
}
if plainM4.Error != 0 {
return newPairingError(plainM4.Error)
}
// STEP M4. Verify response
if !session.VerifyServerAuthenticator(plainM4.Proof) {
return errors.New("hap: wrong server auth")
if !session.VerifyServerAuthenticator([]byte(plainM4.Proof)) {
return errors.New("hap: VerifyServerAuthenticator")
}
// STEP M5. Generate signature
@@ -163,12 +158,12 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
// STEP M5. Generate payload
plainM5 := struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
PublicKey string `tlv8:"3"`
Signature string `tlv8:"10"`
}{
Identifier: c.ClientID,
PublicKey: c.ClientPublic(),
Signature: signature,
PublicKey: string(c.ClientPublic()),
Signature: string(signature),
}
if b, err = tlv8.Marshal(plainM5); err != nil {
return
@@ -188,10 +183,10 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
// STEP M5. Send request
cipherM5 := struct {
EncryptedData []byte `tlv8:"5"`
EncryptedData string `tlv8:"5"`
State byte `tlv8:"6"`
}{
EncryptedData: b,
EncryptedData: string(b),
State: StateM5,
}
if res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(cipherM5)); err != nil {
@@ -200,7 +195,7 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
// STEP M6. Read response
cipherM6 := struct {
EncryptedData []byte `tlv8:"5"`
EncryptedData string `tlv8:"5"`
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
@@ -208,19 +203,19 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
return
}
if cipherM6.State != StateM6 || cipherM6.Error != 0 {
return NewResponseError(plainM5, cipherM6)
return newResponseError(plainM5, cipherM6)
}
// STEP M6. Decrypt payload
b, err = chacha20poly1305.Decrypt(encryptKey, "PS-Msg06", cipherM6.EncryptedData)
b, err = chacha20poly1305.Decrypt(encryptKey, "PS-Msg06", []byte(cipherM6.EncryptedData))
if err != nil {
return
}
plainM6 := struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
PublicKey string `tlv8:"3"`
Signature string `tlv8:"10"`
}{}
if err = tlv8.Unmarshal(b, &plainM6); err != nil {
return
@@ -235,15 +230,15 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
}
b = Append(remoteSign, plainM6.Identifier, plainM6.PublicKey)
if !ed25519.ValidateSignature(plainM6.PublicKey, b, plainM6.Signature) {
return errors.New("hap: wrong accessory sign")
if !ed25519.ValidateSignature([]byte(plainM6.PublicKey), b, []byte(plainM6.Signature)) {
return errors.New("hap: ValidateSignature")
}
if c.DeviceID != plainM6.Identifier {
return errors.New("hap: wrong DeviceID: " + plainM6.Identifier)
}
c.DevicePublic = plainM6.PublicKey
c.DevicePublic = []byte(plainM6.PublicKey)
return nil
}
@@ -264,7 +259,7 @@ func (c *Client) ListPairings() error {
// TODO: don't know how to fix array of items
var plainM2 struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
PublicKey string `tlv8:"3"`
State byte `tlv8:"6"`
Permission byte `tlv8:"11"`
}
@@ -279,13 +274,13 @@ func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) e
plainM1 := struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
PublicKey string `tlv8:"3"`
State byte `tlv8:"6"`
Permission byte `tlv8:"11"`
}{
Method: MethodAddPairing,
Identifier: clientID,
PublicKey: clientPublic,
PublicKey: string(clientPublic),
State: StateM1,
Permission: PermissionUser,
}
@@ -330,7 +325,7 @@ func (c *Client) DeletePairing(id string) error {
return err
}
if plainM2.State != StateM2 {
return NewResponseError(plainM1, plainM2)
return newResponseError(plainM1, plainM2)
}
return nil
+36 -34
View File
@@ -3,12 +3,10 @@ package hap
import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
)
const (
@@ -30,6 +28,12 @@ const (
// - 0100b - A problem has been detected on the accessory
TXTStatusFlags = "sf" // Status flags (ex. 0, 1)
StatusPaired = "0"
StatusNotPaired = "1"
CategoryBridge = "2"
CategoryCamera = "17"
StateM1 = 1
StateM2 = 2
StateM3 = 3
@@ -43,28 +47,41 @@ const (
MethodAddPairing = 3
MethodDeletePairing = 4
MethodListPairings = 5
)
const (
PermissionUser = 0
PermissionAdmin = 1
)
const DeviceAID = 1 // TODO: fix someday
type JSONAccessories struct {
Value []*Accessory `json:"accessories"`
}
type JSONCharacters struct {
Value []JSONCharacter `json:"characteristics"`
}
type JSONCharacter struct {
AID uint8 `json:"aid"`
IID uint64 `json:"iid"`
Value any `json:"value"`
}
func SanitizePin(pin string) (string, error) {
s := strings.ReplaceAll(pin, "-", "")
if len(s) != 8 {
return "", errors.New("hap: wrong PIN format: " + pin)
}
// 123-45-678
return s[:3] + "-" + s[3:5] + "-" + s[5:], nil
}
func GenerateKey() []byte {
_, key, _ := ed25519.GenerateKey(nil)
return key
}
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)
@@ -87,25 +104,10 @@ func Append(items ...any) (b []byte) {
return
}
func NewResponseError(req, res any) error {
func newRequestError(req any) error {
return fmt.Errorf("hap: wrong request: %#v", req)
}
func newResponseError(req, res any) error {
return fmt.Errorf("hap: wrong response: %#v, on request: %#v", res, req)
}
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
}
+27 -35
View File
@@ -85,10 +85,6 @@ func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) {
v := value.Uint()
return append(b, tag, 1, byte(v)), nil
case reflect.Int8:
v := value.Int()
return append(b, tag, 1, byte(v)), nil
case reflect.Uint16:
v := value.Uint()
return append(b, tag, 2, byte(v), byte(v>>8)), nil
@@ -103,7 +99,13 @@ func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) {
case reflect.String:
v := value.String()
b = append(b, tag, byte(len(v)))
l := len(v) // support "big" string
for ; l > 255; l -= 255 {
b = append(b, tag, 255)
b = append(b, v[:255]...)
v = v[255:]
}
b = append(b, tag, byte(l))
return append(b, v...), nil
case reflect.Array:
@@ -117,19 +119,6 @@ func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) {
}
case reflect.Slice:
// byte array
if value.Type().Elem().Kind() == reflect.Uint8 {
v := value.Bytes()
l := len(v)
for ; l > 255; l -= 255 {
b = append(b, tag, 255)
b = append(b, v[:255]...)
v = v[255:]
}
b = append(b, tag, byte(l))
return append(b, v...), nil
}
for i := 0; i < value.Len(); i++ {
if i > 0 {
b = append(b, 0, 0)
@@ -175,24 +164,30 @@ func Unmarshal(data []byte, v any) error {
}
value := reflect.ValueOf(v)
kind := value.Type().Kind()
kind := value.Kind()
if kind != reflect.Pointer {
return errors.New("tlv8: value should be pointer: " + kind.String())
}
value = value.Elem()
kind = value.Type().Kind()
kind = value.Kind()
switch kind {
case reflect.Struct:
return unmarshalStruct(data, value)
if kind == reflect.Interface {
value = value.Elem()
kind = value.Kind()
}
return errors.New("tlv8: not implemented: " + kind.String())
if kind != reflect.Struct {
return errors.New("tlv8: not implemented: " + kind.String())
}
return unmarshalStruct(data, value)
}
func unmarshalStruct(b []byte, value reflect.Value) error {
var waitSlice bool
for len(b) >= 2 {
t := b[0]
l := int(b[1])
@@ -200,6 +195,7 @@ func unmarshalStruct(b []byte, value reflect.Value) error {
// array item divider
if t == 0 && l == 0 {
b = b[2:]
waitSlice = true
continue
}
@@ -228,6 +224,13 @@ func unmarshalStruct(b []byte, value reflect.Value) error {
return fmt.Errorf("tlv8: can't find T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name())
}
if waitSlice {
if valueField.Kind() != reflect.Slice {
return fmt.Errorf("tlv8: should be slice T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name())
}
waitSlice = false
}
if err := unmarshalValue(v, valueField); err != nil {
return err
}
@@ -244,12 +247,6 @@ func unmarshalValue(v []byte, value reflect.Value) error {
}
value.SetUint(uint64(v[0]))
case reflect.Int8:
if len(v) != 1 {
return errors.New("tlv8: wrong size: " + value.Type().Name())
}
value.SetInt(int64(v[0]))
case reflect.Uint16:
if len(v) != 2 {
return errors.New("tlv8: wrong size: " + value.Type().Name())
@@ -280,11 +277,6 @@ func unmarshalValue(v []byte, value reflect.Value) error {
return nil
case reflect.Slice:
if value.Type().Elem().Kind() == reflect.Uint8 {
value.SetBytes(v)
return nil
}
i := growSlice(value)
return unmarshalValue(v, value.Index(i))
+71
View File
@@ -1,6 +1,7 @@
package tlv8
import (
"encoding/hex"
"testing"
"github.com/stretchr/testify/require"
@@ -36,3 +37,73 @@ func TestMarshal(t *testing.T) {
require.Equal(t, src, dst)
}
func TestBytes(t *testing.T) {
bytes := make([]byte, 255)
for i := 0; i < len(bytes); i++ {
bytes[i] = byte(i)
}
type Struct struct {
String string `tlv8:"1"`
}
src := Struct{
String: string(bytes),
}
b, err := Marshal(src)
require.Nil(t, err)
var dst Struct
err = Unmarshal(b, &dst)
require.Nil(t, err)
require.Equal(t, src, dst)
require.Equal(t, bytes, []byte(dst.String))
}
func TestVideoCodecParams(t *testing.T) {
type VideoCodecParams struct {
ProfileID []byte `tlv8:"1"`
Level []byte `tlv8:"2"`
PacketizationMode byte `tlv8:"3"`
CVOEnabled []byte `tlv8:"4"`
CVOID []byte `tlv8:"5"`
}
src, err := hex.DecodeString("0101010201000000020102030100040100")
require.Nil(t, err)
var v VideoCodecParams
err = Unmarshal(src, &v)
require.Nil(t, err)
dst, err := Marshal(v)
require.Nil(t, err)
require.Equal(t, src, dst)
}
func TestInterface(t *testing.T) {
type Struct struct {
Byte byte `tlv8:"1"`
}
src := Struct{
Byte: 1,
}
var v1 any = &src
b, err := Marshal(v1)
require.Nil(t, err)
require.Equal(t, []byte{1, 1, 1}, b)
var dst Struct
var v2 any = &dst
err = Unmarshal(b, v2)
require.Nil(t, err)
require.Equal(t, src, dst)
}