install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# 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
|
||||
|
||||
## AAC ELD
|
||||
|
||||
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
|
||||
- https://github.com/apple/HomeKitADK/blob/master/HAP/HAPPairingPairSetup.c
|
||||
- [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)
|
||||
- [HomeKit Secure Video Unofficial Specification](https://github.com/Supereg/secure-video-specification)
|
||||
- [Homebridge Camera FFmpeg](https://sunoo.github.io/homebridge-camera-ffmpeg/configs/)
|
||||
- https://github.com/ljezny/Particle-HAP/blob/master/HAP-Specification-Non-Commercial-Version.pdf
|
||||
@@ -0,0 +1,176 @@
|
||||
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 uint8 `json:"aid"` // 150 unique accessories per bridge
|
||||
Services []*Service `json:"services"`
|
||||
}
|
||||
|
||||
func (a *Accessory) InitIID() {
|
||||
serviceN := map[string]byte{}
|
||||
for _, service := range a.Services {
|
||||
if len(service.Type) > 3 {
|
||||
panic(service.Type)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 uint64) *Character {
|
||||
for _, serv := range a.Services {
|
||||
for _, char := range serv.Characters {
|
||||
if char.IID == iid {
|
||||
return char
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
Desc string `json:"description,omitempty"`
|
||||
|
||||
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 {
|
||||
for _, char := range s.Characters {
|
||||
if char.Type == charType {
|
||||
return char
|
||||
}
|
||||
}
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
## Useful links
|
||||
|
||||
- https://github.com/bauer-andreas/secure-video-specification
|
||||
@@ -0,0 +1,149 @@
|
||||
package camera
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
|
||||
)
|
||||
|
||||
func NewAccessory(manuf, model, name, serial, firmware string) *hap.Accessory {
|
||||
acc := &hap.Accessory{
|
||||
AID: hap.DeviceAID,
|
||||
Services: []*hap.Service{
|
||||
hap.ServiceAccessoryInformation(manuf, model, name, serial, firmware),
|
||||
ServiceCameraRTPStreamManagement(),
|
||||
//hap.ServiceHAPProtocolInformation(),
|
||||
ServiceMicrophone(),
|
||||
},
|
||||
}
|
||||
acc.InitIID()
|
||||
return acc
|
||||
}
|
||||
|
||||
func ServiceMicrophone() *hap.Service {
|
||||
return &hap.Service{
|
||||
Type: "112", // 'Microphone'
|
||||
Characters: []*hap.Character{
|
||||
{
|
||||
Type: "11A",
|
||||
Format: hap.FormatBool,
|
||||
Value: 0,
|
||||
Perms: hap.EVPRPW,
|
||||
//Descr: "Mute",
|
||||
},
|
||||
//{
|
||||
// Type: "119",
|
||||
// Format: hap.FormatUInt8,
|
||||
// Value: 100,
|
||||
// Perms: hap.EVPRPW,
|
||||
// //Descr: "Volume",
|
||||
// //Unit: hap.UnitPercentage,
|
||||
// //MinValue: 0,
|
||||
// //MaxValue: 100,
|
||||
// //MinStep: 1,
|
||||
//},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ServiceCameraRTPStreamManagement() *hap.Service {
|
||||
val120, _ := tlv8.MarshalBase64(StreamingStatus{
|
||||
Status: StreamingStatusAvailable,
|
||||
})
|
||||
val114, _ := tlv8.MarshalBase64(SupportedVideoStreamConfiguration{
|
||||
Codecs: []VideoCodecConfiguration{
|
||||
{
|
||||
CodecType: VideoCodecTypeH264,
|
||||
CodecParams: []VideoCodecParameters{
|
||||
{
|
||||
ProfileID: []byte{VideoCodecProfileMain},
|
||||
Level: []byte{VideoCodecLevel31, VideoCodecLevel40},
|
||||
},
|
||||
},
|
||||
VideoAttrs: []VideoCodecAttributes{
|
||||
{Width: 1920, Height: 1080, Framerate: 30},
|
||||
{Width: 1280, Height: 720, Framerate: 30}, // important for iPhones
|
||||
{Width: 320, Height: 240, Framerate: 15}, // apple watch
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
val115, _ := tlv8.MarshalBase64(SupportedAudioStreamConfiguration{
|
||||
Codecs: []AudioCodecConfiguration{
|
||||
{
|
||||
CodecType: AudioCodecTypeOpus,
|
||||
CodecParams: []AudioCodecParameters{
|
||||
{
|
||||
Channels: 1,
|
||||
BitrateMode: AudioCodecBitrateVariable,
|
||||
SampleRate: []byte{AudioCodecSampleRate16Khz},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ComfortNoiseSupport: 0,
|
||||
})
|
||||
val116, _ := tlv8.MarshalBase64(SupportedRTPConfiguration{
|
||||
SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80},
|
||||
})
|
||||
|
||||
service := &hap.Service{
|
||||
Type: "110", // 'CameraRTPStreamManagement'
|
||||
Characters: []*hap.Character{
|
||||
{
|
||||
Type: TypeStreamingStatus,
|
||||
Format: hap.FormatTLV8,
|
||||
Value: val120,
|
||||
Perms: hap.EVPR,
|
||||
//Descr: "Streaming Status",
|
||||
},
|
||||
{
|
||||
Type: TypeSupportedVideoStreamConfiguration,
|
||||
Format: hap.FormatTLV8,
|
||||
Value: val114,
|
||||
Perms: hap.PR,
|
||||
//Descr: "Supported Video Stream Configuration",
|
||||
},
|
||||
{
|
||||
Type: TypeSupportedAudioStreamConfiguration,
|
||||
Format: hap.FormatTLV8,
|
||||
Value: val115,
|
||||
Perms: hap.PR,
|
||||
//Descr: "Supported Audio Stream Configuration",
|
||||
},
|
||||
{
|
||||
Type: TypeSupportedRTPConfiguration,
|
||||
Format: hap.FormatTLV8,
|
||||
Value: val116,
|
||||
Perms: hap.PR,
|
||||
//Descr: "Supported RTP Configuration",
|
||||
},
|
||||
{
|
||||
Type: "B0",
|
||||
Format: hap.FormatUInt8,
|
||||
Value: 1,
|
||||
Perms: hap.EVPRPW,
|
||||
//Descr: "Active",
|
||||
//MinValue: 0,
|
||||
//MaxValue: 1,
|
||||
//MinStep: 1,
|
||||
//ValidVal: []any{0, 1},
|
||||
},
|
||||
{
|
||||
Type: TypeSelectedStreamConfiguration,
|
||||
Format: hap.FormatTLV8,
|
||||
Value: "", // important empty
|
||||
Perms: hap.PRPW,
|
||||
//Descr: "Selected RTP Stream Configuration",
|
||||
},
|
||||
{
|
||||
Type: TypeSetupEndpoints,
|
||||
Format: hap.FormatTLV8,
|
||||
Value: "", // important empty
|
||||
Perms: hap.PRPW,
|
||||
//Descr: "Setup Endpoints",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package camera
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNilCharacter(t *testing.T) {
|
||||
var res SetupEndpoints
|
||||
char := &hap.Character{}
|
||||
err := char.ReadTLV8(&res)
|
||||
require.NotNil(t, err)
|
||||
require.NotNil(t, strings.Contains(err.Error(), "can't read value"))
|
||||
}
|
||||
|
||||
type testTLV8 struct {
|
||||
name string
|
||||
value string
|
||||
actual any
|
||||
expect any
|
||||
noequal bool
|
||||
}
|
||||
|
||||
func (test testTLV8) run(t *testing.T) {
|
||||
if test.actual == nil {
|
||||
return
|
||||
}
|
||||
|
||||
src := &hap.Character{Value: test.value, Format: hap.FormatTLV8}
|
||||
err := src.ReadTLV8(test.actual)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, test.expect, test.actual)
|
||||
|
||||
dst := &hap.Character{Format: hap.FormatTLV8}
|
||||
err = dst.Write(test.actual)
|
||||
require.Nil(t, err)
|
||||
|
||||
a, _ := base64.StdEncoding.DecodeString(test.value)
|
||||
b, _ := base64.StdEncoding.DecodeString(dst.Value.(string))
|
||||
t.Logf("%x\n", a)
|
||||
t.Logf("%x\n", b)
|
||||
|
||||
if !test.noequal {
|
||||
require.Equal(t, test.value, dst.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAqaraG3(t *testing.T) {
|
||||
tests := []testTLV8{
|
||||
{
|
||||
name: "120",
|
||||
value: "AQEA",
|
||||
actual: &StreamingStatus{},
|
||||
expect: &StreamingStatus{
|
||||
Status: StreamingStatusAvailable,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "114",
|
||||
value: "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==",
|
||||
actual: &SupportedVideoStreamConfiguration{},
|
||||
expect: &SupportedVideoStreamConfiguration{
|
||||
Codecs: []VideoCodecConfiguration{
|
||||
{
|
||||
CodecType: VideoCodecTypeH264,
|
||||
CodecParams: []VideoCodecParameters{
|
||||
{
|
||||
ProfileID: []byte{VideoCodecProfileMain},
|
||||
Level: []byte{VideoCodecLevel31, VideoCodecLevel40},
|
||||
CVOEnabled: []byte{0},
|
||||
},
|
||||
},
|
||||
VideoAttrs: []VideoCodecAttributes{
|
||||
{Width: 1920, Height: 1080, Framerate: 30},
|
||||
{Width: 1280, Height: 720, Framerate: 30},
|
||||
{Width: 640, Height: 360, Framerate: 30},
|
||||
{Width: 480, Height: 270, Framerate: 30},
|
||||
{Width: 320, Height: 180, Framerate: 30},
|
||||
{Width: 1280, Height: 960, Framerate: 30},
|
||||
{Width: 1024, Height: 768, Framerate: 30},
|
||||
{Width: 640, Height: 480, Framerate: 30},
|
||||
{Width: 480, Height: 360, Framerate: 30},
|
||||
{Width: 320, Height: 240, Framerate: 30},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "115",
|
||||
value: "AQ4BAQICCQEBAQIBAAMBAQIBAA==",
|
||||
actual: &SupportedAudioStreamConfiguration{},
|
||||
expect: &SupportedAudioStreamConfiguration{
|
||||
Codecs: []AudioCodecConfiguration{
|
||||
{
|
||||
CodecType: AudioCodecTypeAACELD,
|
||||
CodecParams: []AudioCodecParameters{
|
||||
{
|
||||
Channels: 1,
|
||||
BitrateMode: AudioCodecBitrateVariable,
|
||||
SampleRate: []byte{AudioCodecSampleRate16Khz},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ComfortNoiseSupport: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "116",
|
||||
value: "AgEAAAACAQEAAAIBAg==",
|
||||
actual: &SupportedRTPConfiguration{},
|
||||
expect: &SupportedRTPConfiguration{
|
||||
SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoAES_CM_256_HMAC_SHA1_80, CryptoDisabled},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, test.run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomebridge(t *testing.T) {
|
||||
tests := []testTLV8{
|
||||
{
|
||||
name: "114",
|
||||
value: "AcUBAQACHQEBAAAAAQEBAAABAQICAQAAAAIBAQAAAgECAwEAAwsBAkABAgK0AAMBHgAAAwsBAkABAgLwAAMBDwAAAwsBAkABAgLwAAMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAoAHAgI4BAMBHgAAAwsBAkAGAgKwBAMBHg==",
|
||||
actual: &SupportedVideoStreamConfiguration{},
|
||||
expect: &SupportedVideoStreamConfiguration{
|
||||
Codecs: []VideoCodecConfiguration{
|
||||
{
|
||||
CodecType: VideoCodecTypeH264,
|
||||
CodecParams: []VideoCodecParameters{
|
||||
{
|
||||
ProfileID: []byte{VideoCodecProfileConstrainedBaseline, VideoCodecProfileMain, VideoCodecProfileHigh},
|
||||
Level: []byte{VideoCodecLevel31, VideoCodecLevel32, VideoCodecLevel40},
|
||||
},
|
||||
},
|
||||
VideoAttrs: []VideoCodecAttributes{
|
||||
|
||||
{Width: 320, Height: 180, Framerate: 30},
|
||||
{Width: 320, Height: 240, Framerate: 15},
|
||||
{Width: 320, Height: 240, Framerate: 30},
|
||||
{Width: 480, Height: 270, Framerate: 30},
|
||||
{Width: 480, Height: 360, Framerate: 30},
|
||||
{Width: 640, Height: 360, Framerate: 30},
|
||||
{Width: 640, Height: 480, Framerate: 30},
|
||||
{Width: 1280, Height: 720, Framerate: 30},
|
||||
{Width: 1280, Height: 960, Framerate: 30},
|
||||
{Width: 1920, Height: 1080, Framerate: 30},
|
||||
{Width: 1600, Height: 1200, Framerate: 30},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "116",
|
||||
value: "AgEA",
|
||||
actual: &SupportedRTPConfiguration{},
|
||||
expect: &SupportedRTPConfiguration{
|
||||
SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, test.run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrypted(t *testing.T) {
|
||||
tests := []testTLV8{
|
||||
{
|
||||
name: "114",
|
||||
value: "AVIBAQACEwEBAQIBAAAAAgEBAAACAQIDAQADCwECAA8CAnAIAwEeAAADCwECgAcCAjgEAwEeAAADCwECAAUCAtACAwEeAAADCwECQAECAvAAAwEP",
|
||||
actual: &SupportedVideoStreamConfiguration{},
|
||||
expect: &SupportedVideoStreamConfiguration{
|
||||
Codecs: []VideoCodecConfiguration{
|
||||
{
|
||||
CodecType: VideoCodecTypeH264,
|
||||
CodecParams: []VideoCodecParameters{
|
||||
{
|
||||
ProfileID: []byte{VideoCodecProfileMain},
|
||||
Level: []byte{VideoCodecLevel31, VideoCodecLevel32, VideoCodecLevel40},
|
||||
},
|
||||
},
|
||||
VideoAttrs: []VideoCodecAttributes{
|
||||
{Width: 3840, Height: 2160, Framerate: 30},
|
||||
{Width: 1920, Height: 1080, Framerate: 30},
|
||||
{Width: 1280, Height: 720, Framerate: 30},
|
||||
{Width: 320, Height: 240, Framerate: 15},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "115",
|
||||
value: "AScBAQMCIgEBAQIBAAMBAAAAAwEAAAADAQEAAAMBAQAAAwECAAADAQICAQA=",
|
||||
actual: &SupportedAudioStreamConfiguration{},
|
||||
expect: &SupportedAudioStreamConfiguration{
|
||||
Codecs: []AudioCodecConfiguration{
|
||||
{
|
||||
CodecType: AudioCodecTypeOpus,
|
||||
CodecParams: []AudioCodecParameters{
|
||||
{
|
||||
Channels: 1,
|
||||
BitrateMode: AudioCodecBitrateVariable,
|
||||
SampleRate: []byte{
|
||||
AudioCodecSampleRate8Khz, AudioCodecSampleRate8Khz,
|
||||
AudioCodecSampleRate16Khz, AudioCodecSampleRate16Khz,
|
||||
AudioCodecSampleRate24Khz, AudioCodecSampleRate24Khz,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ComfortNoiseSupport: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "116",
|
||||
value: "AgEAAAACAQI=",
|
||||
actual: &SupportedRTPConfiguration{},
|
||||
expect: &SupportedRTPConfiguration{
|
||||
SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoDisabled},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, test.run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHass(t *testing.T) {
|
||||
tests := []testTLV8{
|
||||
{
|
||||
name: "114",
|
||||
value: "AdABAQACFQMBAAEBAAEBAQEBAgIBAAIBAQIBAgMMAQJAAQICtAADAg8AAwwBAkABAgLwAAMCDwADDAECQAECArQAAwIeAAMMAQJAAQIC8AADAh4AAwwBAuABAgIOAQMCHgADDAEC4AECAmgBAwIeAAMMAQKAAgICaAEDAh4AAwwBAoACAgLgAQMCHgADDAECAAQCAkACAwIeAAMMAQIABAICAAMDAh4AAwwBAgAFAgLQAgMCHgADDAECAAUCAsADAwIeAAMMAQKABwICOAQDAh4A",
|
||||
},
|
||||
{
|
||||
name: "115",
|
||||
value: "AQ4BAQMCCQEBAQIBAAMBAgEOAQEDAgkBAQECAQADAQECAQA=",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, test.run)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package camera
|
||||
|
||||
const TypeSupportedVideoStreamConfiguration = "114"
|
||||
|
||||
type SupportedVideoStreamConfiguration struct {
|
||||
Codecs []VideoCodecConfiguration `tlv8:"1"`
|
||||
}
|
||||
|
||||
type VideoCodecConfiguration struct {
|
||||
CodecType byte `tlv8:"1"`
|
||||
CodecParams []VideoCodecParameters `tlv8:"2"`
|
||||
VideoAttrs []VideoCodecAttributes `tlv8:"3"`
|
||||
RTPParams []RTPParams `tlv8:"4"`
|
||||
}
|
||||
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
VideoCodecTypeH264 = 0
|
||||
|
||||
VideoCodecProfileConstrainedBaseline = 0
|
||||
VideoCodecProfileMain = 1
|
||||
VideoCodecProfileHigh = 2
|
||||
|
||||
VideoCodecLevel31 = 0
|
||||
VideoCodecLevel32 = 1
|
||||
VideoCodecLevel40 = 2
|
||||
|
||||
VideoCodecPacketizationModeNonInterleaved = 0
|
||||
|
||||
VideoCodecCvoNotSuppported = 0
|
||||
VideoCodecCvoSuppported = 1
|
||||
)
|
||||
|
||||
type VideoCodecParameters 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"` // ID for CVO RTP extensio
|
||||
}
|
||||
|
||||
type VideoCodecAttributes struct {
|
||||
Width uint16 `tlv8:"1"`
|
||||
Height uint16 `tlv8:"2"`
|
||||
Framerate uint8 `tlv8:"3"`
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package camera
|
||||
|
||||
const TypeSupportedAudioStreamConfiguration = "115"
|
||||
|
||||
type SupportedAudioStreamConfiguration struct {
|
||||
Codecs []AudioCodecConfiguration `tlv8:"1"`
|
||||
ComfortNoiseSupport byte `tlv8:"2"`
|
||||
}
|
||||
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
AudioCodecTypePCMU = 0
|
||||
AudioCodecTypePCMA = 1
|
||||
AudioCodecTypeAACELD = 2
|
||||
AudioCodecTypeOpus = 3
|
||||
AudioCodecTypeMSBC = 4
|
||||
AudioCodecTypeAMR = 5
|
||||
AudioCodecTypeARMWB = 6
|
||||
|
||||
AudioCodecBitrateVariable = 0
|
||||
AudioCodecBitrateConstant = 1
|
||||
|
||||
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 AudioCodecConfiguration struct {
|
||||
CodecType byte `tlv8:"1"`
|
||||
CodecParams []AudioCodecParameters `tlv8:"2"`
|
||||
RTPParams []RTPParams `tlv8:"3"`
|
||||
ComfortNoise []byte `tlv8:"4"`
|
||||
}
|
||||
|
||||
type AudioCodecParameters struct {
|
||||
Channels uint8 `tlv8:"1"`
|
||||
BitrateMode 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
|
||||
}
|
||||
@@ -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
|
||||
CryptoDisabled = 2
|
||||
)
|
||||
|
||||
type SupportedRTPConfiguration struct {
|
||||
SRTPCryptoType []byte `tlv8:"2"`
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package camera
|
||||
|
||||
const TypeSelectedStreamConfiguration = "117"
|
||||
|
||||
type SelectedStreamConfiguration struct {
|
||||
Control SessionControl `tlv8:"1"`
|
||||
VideoCodec VideoCodecConfiguration `tlv8:"2"`
|
||||
AudioCodec AudioCodecConfiguration `tlv8:"3"`
|
||||
}
|
||||
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
SessionCommandEnd = 0
|
||||
SessionCommandStart = 1
|
||||
SessionCommandSuspend = 2
|
||||
SessionCommandResume = 3
|
||||
SessionCommandReconfigure = 4
|
||||
)
|
||||
|
||||
type SessionControl struct {
|
||||
SessionID string `tlv8:"1"`
|
||||
Command byte `tlv8:"2"`
|
||||
}
|
||||
|
||||
type RTPParams struct {
|
||||
PayloadType uint8 `tlv8:"1"`
|
||||
SSRC uint32 `tlv8:"2"`
|
||||
MaxBitrate uint16 `tlv8:"3"`
|
||||
RTCPInterval float32 `tlv8:"4"`
|
||||
MaxMTU []uint16 `tlv8:"5"`
|
||||
ComfortNoisePayloadType []uint8 `tlv8:"6"`
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package camera
|
||||
|
||||
const TypeSetupEndpoints = "118"
|
||||
|
||||
type SetupEndpointsRequest struct {
|
||||
SessionID string `tlv8:"1"`
|
||||
Address Address `tlv8:"3"`
|
||||
VideoCrypto SRTPCryptoSuite `tlv8:"4"`
|
||||
AudioCrypto SRTPCryptoSuite `tlv8:"5"`
|
||||
}
|
||||
|
||||
type SetupEndpointsResponse struct {
|
||||
SessionID string `tlv8:"1"`
|
||||
Status byte `tlv8:"2"`
|
||||
Address Address `tlv8:"3"`
|
||||
VideoCrypto SRTPCryptoSuite `tlv8:"4"`
|
||||
AudioCrypto SRTPCryptoSuite `tlv8:"5"`
|
||||
VideoSSRC uint32 `tlv8:"6"`
|
||||
AudioSSRC uint32 `tlv8:"7"`
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
IPVersion byte `tlv8:"1"`
|
||||
IPAddr string `tlv8:"2"`
|
||||
VideoRTPPort uint16 `tlv8:"3"`
|
||||
AudioRTPPort uint16 `tlv8:"4"`
|
||||
}
|
||||
|
||||
type SRTPCryptoSuite struct {
|
||||
CryptoSuite byte `tlv8:"1"`
|
||||
MasterKey string `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM)
|
||||
MasterSalt string `tlv8:"3"` // 14 byte
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package camera
|
||||
|
||||
const TypeStreamingStatus = "120"
|
||||
|
||||
type StreamingStatus struct {
|
||||
Status byte `tlv8:"1"`
|
||||
}
|
||||
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
StreamingStatusAvailable = 0
|
||||
StreamingStatusInUse = 1
|
||||
StreamingStatusUnavailable = 2
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
package camera
|
||||
|
||||
const TypeSupportedDataStreamTransportConfiguration = "130"
|
||||
|
||||
type SupportedDataStreamTransportConfiguration struct {
|
||||
Configs []TransferTransportConfiguration `tlv8:"1"`
|
||||
}
|
||||
|
||||
type TransferTransportConfiguration struct {
|
||||
TransportType byte `tlv8:"1"`
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package camera
|
||||
|
||||
const TypeSetupDataStreamTransport = "131"
|
||||
|
||||
type SetupDataStreamTransportRequest struct {
|
||||
SessionCommandType byte `tlv8:"1"`
|
||||
TransportType byte `tlv8:"2"`
|
||||
ControllerKeySalt string `tlv8:"3"`
|
||||
}
|
||||
|
||||
type SetupDataStreamTransportResponse struct {
|
||||
Status byte `tlv8:"1"`
|
||||
TransportTypeSessionParameters struct {
|
||||
TCPListeningPort uint16 `tlv8:"1"`
|
||||
} `tlv8:"2"`
|
||||
AccessoryKeySalt string `tlv8:"3"`
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package camera
|
||||
|
||||
const TypeSupportedCameraRecordingConfiguration = "205"
|
||||
|
||||
type SupportedCameraRecordingConfiguration struct {
|
||||
PrebufferLength uint32 `tlv8:"1"`
|
||||
EventTriggerOptions uint64 `tlv8:"2"`
|
||||
MediaContainerConfigurations `tlv8:"3"`
|
||||
}
|
||||
|
||||
type MediaContainerConfigurations struct {
|
||||
MediaContainerType uint8 `tlv8:"1"`
|
||||
MediaContainerParameters `tlv8:"2"`
|
||||
}
|
||||
|
||||
type MediaContainerParameters struct {
|
||||
FragmentLength uint32 `tlv8:"1"`
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package camera
|
||||
|
||||
const TypeSupportedVideoRecordingConfiguration = "206"
|
||||
|
||||
type SupportedVideoRecordingConfiguration struct {
|
||||
CodecConfigs []VideoRecordingCodecConfiguration `tlv8:"1"`
|
||||
}
|
||||
|
||||
type VideoRecordingCodecConfiguration struct {
|
||||
CodecType uint8 `tlv8:"1"`
|
||||
CodecParams VideoRecordingCodecParameters `tlv8:"2"`
|
||||
CodecAttrs VideoCodecAttributes `tlv8:"3"`
|
||||
}
|
||||
|
||||
type VideoRecordingCodecParameters struct {
|
||||
ProfileID uint8 `tlv8:"1"`
|
||||
Level uint8 `tlv8:"2"`
|
||||
Bitrate uint32 `tlv8:"3"`
|
||||
IFrameInterval uint32 `tlv8:"4"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package camera
|
||||
|
||||
const TypeSupportedAudioRecordingConfiguration = "207"
|
||||
|
||||
type SupportedAudioRecordingConfiguration struct {
|
||||
CodecConfigs []AudioRecordingCodecConfiguration `tlv8:"1"`
|
||||
}
|
||||
|
||||
type AudioRecordingCodecConfiguration struct {
|
||||
CodecType byte `tlv8:"1"`
|
||||
CodecParams []AudioRecordingCodecParameters `tlv8:"2"`
|
||||
}
|
||||
|
||||
type AudioRecordingCodecParameters struct {
|
||||
Channels uint8 `tlv8:"1"`
|
||||
BitrateMode []byte `tlv8:"2"`
|
||||
SampleRate []byte `tlv8:"3"`
|
||||
MaxAudioBitrate []uint32 `tlv8:"4"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package camera
|
||||
|
||||
const TypeSelectedCameraRecordingConfiguration = "209"
|
||||
|
||||
type SelectedCameraRecordingConfiguration struct {
|
||||
GeneralConfig SupportedCameraRecordingConfiguration `tlv8:"1"`
|
||||
VideoConfig SupportedVideoRecordingConfiguration `tlv8:"2"`
|
||||
AudioConfig SupportedAudioRecordingConfiguration `tlv8:"3"`
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
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 *VideoCodecConfiguration, audioCodec *AudioCodecConfiguration,
|
||||
videoSession, audioSession *srtp.Session, bitrate int,
|
||||
) (*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
|
||||
}
|
||||
|
||||
if bitrate != 0 {
|
||||
bitrate /= 1024 // convert bps to kbps
|
||||
} else {
|
||||
bitrate = 4096 // default kbps for general FullHD camera
|
||||
}
|
||||
|
||||
videoCodec.RTPParams = []RTPParams{
|
||||
{
|
||||
PayloadType: 99,
|
||||
SSRC: videoSession.Local.SSRC,
|
||||
MaxBitrate: uint16(bitrate), // iPhone query 299Kbps, iPad/AppleTV query 802Kbps
|
||||
RTCPInterval: 0.5,
|
||||
MaxMTU: []uint16{1378},
|
||||
},
|
||||
}
|
||||
audioCodec.RTPParams = []RTPParams{
|
||||
{
|
||||
PayloadType: 110,
|
||||
SSRC: audioSession.Local.SSRC,
|
||||
MaxBitrate: 24, // any iDevice query 24Kbps (this is OK for 16KHz and 1 channel)
|
||||
RTCPInterval: 5,
|
||||
|
||||
ComfortNoisePayloadType: []uint8{13},
|
||||
},
|
||||
}
|
||||
audioCodec.ComfortNoise = []byte{0}
|
||||
|
||||
config := &SelectedStreamConfiguration{
|
||||
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 := SetupEndpointsRequest{
|
||||
SessionID: s.id,
|
||||
Address: Address{
|
||||
IPVersion: 0,
|
||||
IPAddr: videoSession.Local.Addr,
|
||||
VideoRTPPort: videoSession.Local.Port,
|
||||
AudioRTPPort: audioSession.Local.Port,
|
||||
},
|
||||
VideoCrypto: SRTPCryptoSuite{
|
||||
MasterKey: string(videoSession.Local.MasterKey),
|
||||
MasterSalt: string(videoSession.Local.MasterSalt),
|
||||
},
|
||||
AudioCrypto: SRTPCryptoSuite{
|
||||
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 SetupEndpointsResponse
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stream) SetStreamConfig(config *SelectedStreamConfiguration) 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 := &SelectedStreamConfiguration{
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package chacha20poly1305
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
var ErrInvalidParams = errors.New("chacha20poly1305: invalid params")
|
||||
|
||||
// Decrypt - decrypt without verify
|
||||
func Decrypt(key32 []byte, nonce8 string, ciphertext []byte) ([]byte, error) {
|
||||
return DecryptAndVerify(key32, nil, []byte(nonce8), ciphertext, nil)
|
||||
}
|
||||
|
||||
// Encrypt - encrypt without seal
|
||||
func Encrypt(key32 []byte, nonce8 string, plaintext []byte) ([]byte, error) {
|
||||
return EncryptAndSeal(key32, nil, []byte(nonce8), plaintext, nil)
|
||||
}
|
||||
|
||||
func DecryptAndVerify(key32, dst, nonce8, ciphertext, verify []byte) ([]byte, error) {
|
||||
if len(key32) != chacha20poly1305.KeySize || len(nonce8) != 8 {
|
||||
return nil, ErrInvalidParams
|
||||
}
|
||||
|
||||
aead, err := chacha20poly1305.New(key32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, chacha20poly1305.NonceSize)
|
||||
copy(nonce[4:], nonce8)
|
||||
|
||||
return aead.Open(dst, nonce, ciphertext, verify)
|
||||
}
|
||||
|
||||
func EncryptAndSeal(key32, dst, nonce8, plaintext, verify []byte) ([]byte, error) {
|
||||
if len(key32) != chacha20poly1305.KeySize || len(nonce8) != 8 {
|
||||
return nil, ErrInvalidParams
|
||||
}
|
||||
|
||||
aead, err := chacha20poly1305.New(key32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, chacha20poly1305.NonceSize)
|
||||
copy(nonce[4:], nonce8)
|
||||
|
||||
return aead.Seal(dst, nonce, plaintext, verify), nil
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package hap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"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 {
|
||||
Desc string `json:"description,omitempty"`
|
||||
|
||||
IID uint64 `json:"iid"`
|
||||
Type string `json:"type"`
|
||||
Format string `json:"format"`
|
||||
Value any `json:"value,omitempty"`
|
||||
Perms []string `json:"perms"`
|
||||
|
||||
//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
|
||||
}
|
||||
|
||||
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) {
|
||||
v := JSONCharacters{
|
||||
Value: []JSONCharacter{
|
||||
{AID: DeviceAID, IID: c.IID, Value: c.Value},
|
||||
},
|
||||
}
|
||||
if data, err = json.Marshal(v); 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 any) (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 any) (err error) {
|
||||
switch c.Format {
|
||||
case "tlv8":
|
||||
c.Value, err = tlv8.MarshalBase64(v)
|
||||
|
||||
case "bool":
|
||||
switch v := v.(type) {
|
||||
case bool:
|
||||
c.Value = v
|
||||
case float64:
|
||||
c.Value = v != 0
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ReadTLV8 value to right struct
|
||||
func (c *Character) ReadTLV8(v any) (err error) {
|
||||
if s, ok := c.Value.(string); ok {
|
||||
return tlv8.UnmarshalBase64(s, v)
|
||||
}
|
||||
return fmt.Errorf("hap: can't read value: %v", v)
|
||||
}
|
||||
|
||||
func (c *Character) ReadBool() (bool, error) {
|
||||
if v, ok := c.Value.(bool); ok {
|
||||
return v, nil
|
||||
}
|
||||
return false, fmt.Errorf("hap: can't read value: %v", c.Value)
|
||||
}
|
||||
|
||||
func (c *Character) String() string {
|
||||
data, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return "ERROR"
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
package hap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/curve25519"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/ed25519"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
||||
)
|
||||
|
||||
const (
|
||||
ConnDialTimeout = time.Second * 3
|
||||
ConnDeadline = time.Second * 3
|
||||
)
|
||||
|
||||
// Client for HomeKit. DevicePublic can be null.
|
||||
type Client struct {
|
||||
DeviceAddress string // including port
|
||||
DeviceID string // aka. Accessory
|
||||
DevicePublic []byte
|
||||
ClientID string // aka. Controller
|
||||
ClientPrivate []byte
|
||||
|
||||
OnEvent func(res *http.Response)
|
||||
//Output func(msg any)
|
||||
|
||||
Conn net.Conn
|
||||
reader *bufio.Reader
|
||||
|
||||
res chan *http.Response
|
||||
err error
|
||||
}
|
||||
|
||||
func Dial(rawURL string) (*Client, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
c := &Client{
|
||||
DeviceAddress: u.Host,
|
||||
DeviceID: query.Get("device_id"),
|
||||
DevicePublic: DecodeKey(query.Get("device_public")),
|
||||
ClientID: query.Get("client_id"),
|
||||
ClientPrivate: DecodeKey(query.Get("client_private")),
|
||||
}
|
||||
|
||||
if err = c.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Client) ClientPublic() []byte {
|
||||
return c.ClientPrivate[32:]
|
||||
}
|
||||
|
||||
func (c *Client) 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 *Client) DeviceHost() string {
|
||||
if i := strings.IndexByte(c.DeviceAddress, ':'); i > 0 {
|
||||
return c.DeviceAddress[:i]
|
||||
}
|
||||
return c.DeviceAddress
|
||||
}
|
||||
|
||||
func (c *Client) Dial() (err error) {
|
||||
if len(c.ClientID) == 0 || len(c.ClientPrivate) == 0 {
|
||||
return errors.New("hap: can't dial witout client_id or client_private")
|
||||
}
|
||||
|
||||
// update device address (host and/or port) before dial
|
||||
_ = mdns.QueryOrDiscovery(c.DeviceHost(), mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
||||
if entry.Complete() && entry.Info["id"] == c.DeviceID {
|
||||
c.DeviceAddress = entry.Addr()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// TODO: close conn on error
|
||||
if c.Conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.reader = bufio.NewReader(c.Conn)
|
||||
|
||||
// STEP M1: send our session public to device
|
||||
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
|
||||
|
||||
// 1. Send sessionPublic
|
||||
plainM1 := struct {
|
||||
PublicKey string `tlv8:"3"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
PublicKey: string(sessionPublic),
|
||||
State: StateM1,
|
||||
}
|
||||
res, err := c.Post(PathPairVerify, MimeTLV8, tlv8.MarshalReader(plainM1))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M2: unpack deviceID from response
|
||||
var cipherM2 struct {
|
||||
PublicKey string `tlv8:"3"`
|
||||
EncryptedData string `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
}
|
||||
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &cipherM2); err != nil {
|
||||
return err
|
||||
}
|
||||
if cipherM2.State != StateM2 {
|
||||
return newResponseError(plainM1, cipherM2)
|
||||
}
|
||||
|
||||
// 1. generate session shared key
|
||||
sessionShared, err := curve25519.SharedSecret(sessionPrivate, []byte(cipherM2.PublicKey))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sessionKey, err := hkdf.Sha512(
|
||||
sessionShared, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info",
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. decrypt M2 response with session key
|
||||
b, err := chacha20poly1305.Decrypt(sessionKey, "PV-Msg02", []byte(cipherM2.EncryptedData))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 3. unpack payload from TLV8
|
||||
var plainM2 struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
Signature string `tlv8:"10"`
|
||||
}
|
||||
if err = tlv8.Unmarshal(b, &plainM2); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 4. verify signature for M2 response with device public
|
||||
// device session + device id + our session
|
||||
if c.DevicePublic != nil {
|
||||
b = Append(cipherM2.PublicKey, plainM2.Identifier, sessionPublic)
|
||||
if !ed25519.ValidateSignature(c.DevicePublic, b, []byte(plainM2.Signature)) {
|
||||
return errors.New("hap: ValidateSignature")
|
||||
}
|
||||
}
|
||||
|
||||
// STEP M3: send our clientID to device
|
||||
// 1. generate signature with our private key
|
||||
// (our session + our ID + device session)
|
||||
b = Append(sessionPublic, c.ClientID, cipherM2.PublicKey)
|
||||
if b, err = ed25519.Signature(c.ClientPrivate, b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. generate payload
|
||||
plainM3 := struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
Signature string `tlv8:"10"`
|
||||
}{
|
||||
Identifier: c.ClientID,
|
||||
Signature: string(b),
|
||||
}
|
||||
if b, err = tlv8.Marshal(plainM3); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 4. encrypt payload with session key
|
||||
if b, err = chacha20poly1305.Encrypt(sessionKey, "PV-Msg03", b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 4. generate request
|
||||
cipherM3 := struct {
|
||||
EncryptedData string `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
State: StateM3,
|
||||
EncryptedData: string(b),
|
||||
}
|
||||
if res, err = c.Post(PathPairVerify, MimeTLV8, tlv8.MarshalReader(cipherM3)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M4. Read response
|
||||
var plainM4 struct {
|
||||
State byte `tlv8:"6"`
|
||||
}
|
||||
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM4); err != nil {
|
||||
return
|
||||
}
|
||||
if plainM4.State != StateM4 {
|
||||
return newResponseError(cipherM3, plainM4)
|
||||
}
|
||||
|
||||
rw := bufio.NewReadWriter(c.reader, bufio.NewWriter(c.Conn))
|
||||
|
||||
// like tls.Client wrapper over net.Conn
|
||||
if c.Conn, err = NewConn(c.Conn, rw, sessionShared, true); err != nil {
|
||||
return
|
||||
}
|
||||
// new reader for new conn
|
||||
c.reader = bufio.NewReader(c.Conn)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
if c.Conn == nil {
|
||||
return nil
|
||||
}
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *Client) eventsReader() {
|
||||
c.res = make(chan *http.Response)
|
||||
|
||||
for {
|
||||
var res *http.Response
|
||||
if res, c.err = ReadResponse(c.reader, nil); c.err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var body []byte
|
||||
if body, c.err = io.ReadAll(res.Body); c.err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
res.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
if res.Proto != ProtoEvent {
|
||||
c.res <- res
|
||||
} else if c.OnEvent != nil {
|
||||
c.OnEvent(res)
|
||||
}
|
||||
}
|
||||
|
||||
close(c.res)
|
||||
}
|
||||
|
||||
func (c *Client) GetAccessories() ([]*Accessory, error) {
|
||||
res, err := c.Get(PathAccessories)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var v JSONAccessories
|
||||
if err = json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.Value, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var v JSONCharacters
|
||||
if err = json.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.Value, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetCharacter(char *Character) error {
|
||||
query := fmt.Sprintf("%d.%d", DeviceAID, char.IID)
|
||||
chars, err := c.GetCharacters(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
char.Value = chars[0].Value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) PutCharacters(characters ...*Character) error {
|
||||
var v JSONCharacters
|
||||
for i, char := range characters {
|
||||
v.Value = append(v.Value, JSONCharacter{
|
||||
AID: 1,
|
||||
IID: char.IID,
|
||||
Value: char.Value,
|
||||
})
|
||||
characters[i] = char
|
||||
}
|
||||
body, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = io.ReadAll(res.Body) // important to "clear" body
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetImage(width, height int) ([]byte, error) {
|
||||
s := fmt.Sprintf(
|
||||
`{"image-width":%d,"image-height":%d,"resource-type":"image","reason":0}`,
|
||||
width, height,
|
||||
)
|
||||
res, err := c.Post(PathResource, MimeJSON, bytes.NewBufferString(s))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
|
||||
func (c *Client) LocalIP() string {
|
||||
if c.Conn == nil {
|
||||
return ""
|
||||
}
|
||||
addr := c.Conn.LocalAddr().(*net.TCPAddr)
|
||||
return addr.IP.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,101 @@
|
||||
package hap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
MimeTLV8 = "application/pairing+tlv8"
|
||||
MimeJSON = "application/hap+json"
|
||||
|
||||
PathPairSetup = "/pair-setup"
|
||||
PathPairVerify = "/pair-verify"
|
||||
PathPairings = "/pairings"
|
||||
PathAccessories = "/accessories"
|
||||
PathCharacteristics = "/characteristics"
|
||||
PathResource = "/resource"
|
||||
)
|
||||
|
||||
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
if err := req.Write(c.Conn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.res != nil {
|
||||
return <-c.res, c.err
|
||||
}
|
||||
return http.ReadResponse(c.reader, req)
|
||||
}
|
||||
|
||||
func (c *Client) Request(method, path, contentType string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequest(method, "http://"+c.DeviceAddress+path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
|
||||
res, err := c.Do(req)
|
||||
if err == nil && res.StatusCode >= http.StatusBadRequest {
|
||||
err = errors.New("hap: wrong http status: " + res.Status)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (c *Client) Get(path string) (*http.Response, error) {
|
||||
return c.Request("GET", path, "", nil)
|
||||
}
|
||||
|
||||
func (c *Client) Post(path, contentType string, body io.Reader) (*http.Response, error) {
|
||||
return c.Request("POST", path, contentType, body)
|
||||
}
|
||||
|
||||
func (c *Client) Put(path, contentType string, body io.Reader) (*http.Response, error) {
|
||||
return c.Request("PUT", path, contentType, body)
|
||||
}
|
||||
|
||||
const ProtoEvent = "EVENT/1.0"
|
||||
|
||||
func ReadResponse(r *bufio.Reader, req *http.Request) (*http.Response, error) {
|
||||
b, err := r.Peek(9)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if string(b) != ProtoEvent {
|
||||
return http.ReadResponse(r, req)
|
||||
}
|
||||
|
||||
copy(b, "HTTP/1.1 ")
|
||||
|
||||
res, err := http.ReadResponse(r, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.Proto = ProtoEvent
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func WriteEvent(w io.Writer, res *http.Response) error {
|
||||
return res.Write(&eventWriter{w: w})
|
||||
}
|
||||
|
||||
type eventWriter struct {
|
||||
w io.Writer
|
||||
done bool
|
||||
}
|
||||
|
||||
func (e *eventWriter) Write(p []byte) (n int, err error) {
|
||||
if !e.done {
|
||||
p = append([]byte("EVENT/1.0"), p[8:]...)
|
||||
e.done = true
|
||||
}
|
||||
return e.w.Write(p)
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package hap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/ed25519"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
|
||||
"github.com/tadglines/go-pkgs/crypto/srp"
|
||||
)
|
||||
|
||||
// Pair homekit
|
||||
func Pair(rawURL string) (*Client, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
|
||||
c := &Client{
|
||||
DeviceAddress: u.Host,
|
||||
DeviceID: query.Get("device_id"),
|
||||
ClientID: query.Get("client_id"),
|
||||
ClientPrivate: DecodeKey(query.Get("client_private")),
|
||||
}
|
||||
|
||||
if c.ClientID == "" {
|
||||
c.ClientID = GenerateUUID()
|
||||
}
|
||||
if c.ClientPrivate == nil {
|
||||
c.ClientPrivate = GenerateKey()
|
||||
}
|
||||
|
||||
if err = c.Pair(query.Get("feature"), query.Get("pin")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func Unpair(rawURL string) error {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
conn := &Client{
|
||||
DeviceAddress: u.Host,
|
||||
DeviceID: query.Get("device_id"),
|
||||
DevicePublic: DecodeKey(query.Get("device_public")),
|
||||
ClientID: query.Get("client_id"),
|
||||
ClientPrivate: DecodeKey(query.Get("client_private")),
|
||||
}
|
||||
|
||||
if err = conn.Dial(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
if err = conn.ListPairings(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return conn.DeletePairing(conn.ClientID)
|
||||
}
|
||||
|
||||
func (c *Client) Pair(feature, pin string) (err error) {
|
||||
if pin, err = SanitizePin(pin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.reader = bufio.NewReader(c.Conn)
|
||||
|
||||
// STEP M1. Send HELLO
|
||||
plainM1 := struct {
|
||||
Method byte `tlv8:"0"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
Method: MethodPair,
|
||||
State: StateM1,
|
||||
}
|
||||
if feature == "1" {
|
||||
plainM1.Method = MethodPairMFi // ff=1 => method=1, ff=2 => method=0
|
||||
}
|
||||
res, err := c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(plainM1))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M2. Read Device Salt and session PublicKey
|
||||
var plainM2 struct {
|
||||
Salt string `tlv8:"2"`
|
||||
SessionKey string `tlv8:"3"` // server public key, aka session.B
|
||||
State byte `tlv8:"6"`
|
||||
Error byte `tlv8:"7"`
|
||||
}
|
||||
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil {
|
||||
return
|
||||
}
|
||||
if plainM2.State != StateM2 {
|
||||
return newResponseError(plainM1, plainM2)
|
||||
}
|
||||
if plainM2.Error != 0 {
|
||||
return newPairingError(plainM2.Error)
|
||||
}
|
||||
|
||||
// STEP M3. Generate SRP Session using pin
|
||||
username := []byte("Pair-Setup")
|
||||
|
||||
// Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE)
|
||||
pake, err := srp.NewSRP("rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pake.SaltLength = 16
|
||||
|
||||
// username: "Pair-Setup", password: PIN (with dashes)
|
||||
session := pake.NewClientSession(username, []byte(pin))
|
||||
|
||||
sessionShared, err := session.ComputeKey([]byte(plainM2.Salt), []byte(plainM2.SessionKey))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M3. Send request
|
||||
plainM3 := struct {
|
||||
SessionKey string `tlv8:"3"`
|
||||
Proof string `tlv8:"4"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
SessionKey: string(session.GetA()), // client public key, aka session.A
|
||||
Proof: string(session.ComputeAuthenticator()),
|
||||
State: StateM3,
|
||||
}
|
||||
if res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(plainM3)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M4. Read response
|
||||
var plainM4 struct {
|
||||
Proof string `tlv8:"4"` // server proof
|
||||
State byte `tlv8:"6"`
|
||||
Error byte `tlv8:"7"`
|
||||
|
||||
EncryptedData string `tlv8:"5"` // skip EncryptedData validation (for MFi devices)
|
||||
}
|
||||
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM4); err != nil {
|
||||
return
|
||||
}
|
||||
if plainM4.State != StateM4 {
|
||||
return newResponseError(plainM3, plainM4)
|
||||
}
|
||||
if plainM4.Error != 0 {
|
||||
return newPairingError(plainM4.Error)
|
||||
}
|
||||
|
||||
// STEP M4. Verify response
|
||||
if !session.VerifyServerAuthenticator([]byte(plainM4.Proof)) {
|
||||
return errors.New("hap: VerifyServerAuthenticator")
|
||||
}
|
||||
|
||||
// STEP M5. Generate signature
|
||||
localSign, err := hkdf.Sha512(
|
||||
sessionShared, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info",
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b := Append(localSign, c.ClientID, c.ClientPublic())
|
||||
signature, err := ed25519.Signature(c.ClientPrivate, b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M5. Generate payload
|
||||
plainM5 := struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
Signature string `tlv8:"10"`
|
||||
}{
|
||||
Identifier: c.ClientID,
|
||||
PublicKey: string(c.ClientPublic()),
|
||||
Signature: string(signature),
|
||||
}
|
||||
if b, err = tlv8.Marshal(plainM5); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M5. Encrypt payload
|
||||
encryptKey, err := hkdf.Sha512(
|
||||
sessionShared, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info",
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if b, err = chacha20poly1305.Encrypt(encryptKey, "PS-Msg05", b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M5. Send request
|
||||
cipherM5 := struct {
|
||||
EncryptedData string `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
EncryptedData: string(b),
|
||||
State: StateM5,
|
||||
}
|
||||
if res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(cipherM5)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M6. Read response
|
||||
cipherM6 := struct {
|
||||
EncryptedData string `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
Error byte `tlv8:"7"`
|
||||
}{}
|
||||
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &cipherM6); err != nil {
|
||||
return
|
||||
}
|
||||
if cipherM6.State != StateM6 || cipherM6.Error != 0 {
|
||||
return newResponseError(plainM5, cipherM6)
|
||||
}
|
||||
|
||||
// STEP M6. Decrypt payload
|
||||
b, err = chacha20poly1305.Decrypt(encryptKey, "PS-Msg06", []byte(cipherM6.EncryptedData))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
plainM6 := struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
Signature string `tlv8:"10"`
|
||||
}{}
|
||||
if err = tlv8.Unmarshal(b, &plainM6); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M6. Verify payload
|
||||
remoteSign, err := hkdf.Sha512(
|
||||
sessionShared, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info",
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b = Append(remoteSign, plainM6.Identifier, plainM6.PublicKey)
|
||||
if !ed25519.ValidateSignature([]byte(plainM6.PublicKey), b, []byte(plainM6.Signature)) {
|
||||
return errors.New("hap: ValidateSignature")
|
||||
}
|
||||
|
||||
if c.DeviceID != plainM6.Identifier {
|
||||
return errors.New("hap: wrong DeviceID: " + plainM6.Identifier)
|
||||
}
|
||||
|
||||
c.DevicePublic = []byte(plainM6.PublicKey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ListPairings() error {
|
||||
plainM1 := struct {
|
||||
Method byte `tlv8:"0"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
Method: MethodListPairings,
|
||||
State: StateM1,
|
||||
}
|
||||
res, err := c.Post(PathPairings, MimeTLV8, tlv8.MarshalReader(plainM1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: don't know how to fix array of items
|
||||
var plainM2 struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
State byte `tlv8:"6"`
|
||||
Permission byte `tlv8:"11"`
|
||||
}
|
||||
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) error {
|
||||
plainM1 := struct {
|
||||
Method byte `tlv8:"0"`
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
State byte `tlv8:"6"`
|
||||
Permission byte `tlv8:"11"`
|
||||
}{
|
||||
Method: MethodAddPairing,
|
||||
Identifier: clientID,
|
||||
PublicKey: string(clientPublic),
|
||||
State: StateM1,
|
||||
Permission: PermissionUser,
|
||||
}
|
||||
if admin {
|
||||
plainM1.Permission = PermissionAdmin
|
||||
}
|
||||
res, err := c.Post(PathPairings, MimeTLV8, tlv8.MarshalReader(plainM1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var plainM2 struct {
|
||||
State byte `tlv8:"6"`
|
||||
Unknown byte `tlv8:"7"`
|
||||
}
|
||||
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) DeletePairing(id string) error {
|
||||
plainM1 := struct {
|
||||
Method byte `tlv8:"0"`
|
||||
Identifier string `tlv8:"1"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
Method: MethodDeletePairing,
|
||||
Identifier: id,
|
||||
State: StateM1,
|
||||
}
|
||||
res, err := c.Post(PathPairings, MimeTLV8, tlv8.MarshalReader(plainM1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var plainM2 struct {
|
||||
State byte `tlv8:"6"`
|
||||
}
|
||||
if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil {
|
||||
return err
|
||||
}
|
||||
if plainM2.State != StateM2 {
|
||||
return newResponseError(plainM1, plainM2)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newPairingError(code byte) error {
|
||||
var text string
|
||||
// https://github.com/apple/HomeKitADK/blob/fb201f98f5fdc7fef6a455054f08b59cca5d1ec8/HAP/HAPPairing.h#L89
|
||||
switch code {
|
||||
case 1:
|
||||
text = "Generic error to handle unexpected errors"
|
||||
case 2:
|
||||
text = "Setup code or signature verification failed"
|
||||
case 3:
|
||||
text = "Client must look at the retry delay TLV item and wait that many seconds before retrying"
|
||||
case 4:
|
||||
text = "Server cannot accept any more pairings"
|
||||
case 5:
|
||||
text = "Server reached its maximum number of authentication attempts"
|
||||
case 6:
|
||||
text = "Server pairing method is unavailable"
|
||||
case 7:
|
||||
text = "Server is busy and cannot accept a pairing request at this time"
|
||||
default:
|
||||
text = "Unknown pairing error"
|
||||
}
|
||||
return errors.New("hap: " + text)
|
||||
}
|
||||
|
||||
func keyDerivativeFuncRFC2945(username []byte) srp.KeyDerivationFunc {
|
||||
return func(salt, password []byte) []byte {
|
||||
h1 := sha512.New()
|
||||
h1.Write(username)
|
||||
h1.Write([]byte(":"))
|
||||
h1.Write(password)
|
||||
|
||||
h2 := sha512.New()
|
||||
h2.Write(salt)
|
||||
h2.Write(h1.Sum(nil))
|
||||
|
||||
return h2.Sum(nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package hap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
conn net.Conn
|
||||
rw *bufio.ReadWriter
|
||||
wmu sync.Mutex
|
||||
|
||||
encryptKey []byte
|
||||
decryptKey []byte
|
||||
encryptCnt uint64
|
||||
decryptCnt uint64
|
||||
|
||||
//ClientID string
|
||||
SharedKey []byte
|
||||
|
||||
recv int
|
||||
send int
|
||||
}
|
||||
|
||||
func (c *Conn) MarshalJSON() ([]byte, error) {
|
||||
conn := core.Connection{
|
||||
ID: core.ID(c),
|
||||
FormatName: "homekit",
|
||||
Protocol: "hap",
|
||||
RemoteAddr: c.conn.RemoteAddr().String(),
|
||||
Recv: c.recv,
|
||||
Send: c.send,
|
||||
}
|
||||
return json.Marshal(conn)
|
||||
}
|
||||
|
||||
func NewConn(conn net.Conn, rw *bufio.ReadWriter, sharedKey []byte, isClient bool) (*Conn, error) {
|
||||
key1, err := hkdf.Sha512(sharedKey, "Control-Salt", "Control-Read-Encryption-Key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key2, err := hkdf.Sha512(sharedKey, "Control-Salt", "Control-Write-Encryption-Key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Conn{
|
||||
conn: conn,
|
||||
rw: rw,
|
||||
|
||||
SharedKey: sharedKey,
|
||||
}
|
||||
|
||||
if isClient {
|
||||
c.encryptKey, c.decryptKey = key2, key1
|
||||
} else {
|
||||
c.encryptKey, c.decryptKey = key1, key2
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// packetSizeMax is the max length of encrypted packets
|
||||
packetSizeMax = 0x400
|
||||
|
||||
VerifySize = 2
|
||||
NonceSize = 8
|
||||
Overhead = 16 // chacha20poly1305.Overhead
|
||||
)
|
||||
|
||||
func (c *Conn) Read(b []byte) (n int, err error) {
|
||||
if cap(b) < packetSizeMax {
|
||||
return 0, errors.New("hap: read buffer is too small")
|
||||
}
|
||||
|
||||
verify := make([]byte, VerifySize) // verify = plain message size
|
||||
if _, err = io.ReadFull(c.rw, verify); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n = int(binary.LittleEndian.Uint16(verify))
|
||||
|
||||
ciphertext := make([]byte, n+Overhead)
|
||||
if _, err = io.ReadFull(c.rw, ciphertext); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
nonce := make([]byte, NonceSize)
|
||||
binary.LittleEndian.PutUint64(nonce, c.decryptCnt)
|
||||
c.decryptCnt++
|
||||
|
||||
_, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify)
|
||||
|
||||
c.recv += n
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) Write(b []byte) (n int, err error) {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
|
||||
buf := make([]byte, 0, packetSizeMax+Overhead)
|
||||
nonce := make([]byte, NonceSize)
|
||||
verify := make([]byte, VerifySize)
|
||||
|
||||
for len(b) > 0 {
|
||||
size := len(b)
|
||||
if size > packetSizeMax {
|
||||
size = packetSizeMax
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint16(verify, uint16(size))
|
||||
if _, err = c.rw.Write(verify); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint64(nonce, c.encryptCnt)
|
||||
c.encryptCnt++
|
||||
|
||||
_, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf, nonce, b[:size], verify)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = c.rw.Write(buf[:size+Overhead]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b = b[size:]
|
||||
n += size
|
||||
}
|
||||
|
||||
err = c.rw.Flush()
|
||||
|
||||
c.send += n
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) LocalAddr() net.Addr {
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *Conn) RemoteAddr() net.Addr {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *Conn) SetDeadline(t time.Time) error {
|
||||
return c.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *Conn) SetReadDeadline(t time.Time) error {
|
||||
return c.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *Conn) SetWriteDeadline(t time.Time) error {
|
||||
return c.conn.SetWriteDeadline(t)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package curve25519
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
func GenerateKeyPair() ([]byte, []byte) {
|
||||
var publicKey, privateKey [32]byte
|
||||
_, _ = rand.Read(privateKey[:])
|
||||
curve25519.ScalarBaseMult(&publicKey, &privateKey)
|
||||
return publicKey[:], privateKey[:]
|
||||
}
|
||||
|
||||
func SharedSecret(privateKey, otherPublicKey []byte) ([]byte, error) {
|
||||
return curve25519.X25519(privateKey, otherPublicKey)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package ed25519
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var ErrInvalidParams = errors.New("ed25519: invalid params")
|
||||
|
||||
func ValidateSignature(key, data, signature []byte) bool {
|
||||
if len(key) != ed25519.PublicKeySize || len(signature) != ed25519.SignatureSize {
|
||||
return false
|
||||
}
|
||||
|
||||
return ed25519.Verify(key, data, signature)
|
||||
}
|
||||
|
||||
func Signature(key, data []byte) ([]byte, error) {
|
||||
if len(key) != ed25519.PrivateKeySize {
|
||||
return nil, ErrInvalidParams
|
||||
}
|
||||
|
||||
return ed25519.Sign(key, data), nil
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// Package hds - HomeKit Data Stream
|
||||
package hds
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
|
||||
)
|
||||
|
||||
func NewConn(conn net.Conn, key []byte, salt string, controller bool) (*Conn, error) {
|
||||
writeKey, err := hkdf.Sha512(key, salt, "HDS-Write-Encryption-Key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
readKey, err := hkdf.Sha512(key, salt, "HDS-Read-Encryption-Key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Conn{
|
||||
conn: conn,
|
||||
rd: bufio.NewReaderSize(conn, 32*1024),
|
||||
wr: bufio.NewWriterSize(conn, 32*1024),
|
||||
}
|
||||
|
||||
if controller {
|
||||
c.decryptKey, c.encryptKey = readKey, writeKey
|
||||
} else {
|
||||
c.decryptKey, c.encryptKey = writeKey, readKey
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
conn net.Conn
|
||||
|
||||
rd *bufio.Reader
|
||||
wr *bufio.Writer
|
||||
|
||||
decryptKey []byte
|
||||
encryptKey []byte
|
||||
decryptCnt uint64
|
||||
encryptCnt uint64
|
||||
|
||||
recv int
|
||||
send int
|
||||
}
|
||||
|
||||
func (c *Conn) MarshalJSON() ([]byte, error) {
|
||||
conn := core.Connection{
|
||||
ID: core.ID(c),
|
||||
FormatName: "homekit",
|
||||
Protocol: "hds",
|
||||
RemoteAddr: c.conn.RemoteAddr().String(),
|
||||
Recv: c.recv,
|
||||
Send: c.send,
|
||||
}
|
||||
return json.Marshal(conn)
|
||||
}
|
||||
|
||||
func (c *Conn) read() (b []byte, err error) {
|
||||
verify := make([]byte, 4)
|
||||
if _, err = io.ReadFull(c.rd, verify); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n := int(binary.BigEndian.Uint32(verify) & 0xFFFFFF)
|
||||
|
||||
ciphertext := make([]byte, n+hap.Overhead)
|
||||
if _, err = io.ReadFull(c.rd, ciphertext); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
nonce := make([]byte, hap.NonceSize)
|
||||
binary.LittleEndian.PutUint64(nonce, c.decryptCnt)
|
||||
c.decryptCnt++
|
||||
|
||||
c.recv += n
|
||||
|
||||
return chacha20poly1305.DecryptAndVerify(c.decryptKey, ciphertext[:0], nonce, ciphertext, verify)
|
||||
}
|
||||
|
||||
func (c *Conn) Read(p []byte) (n int, err error) {
|
||||
b, err := c.read()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n = copy(p, b)
|
||||
if len(b) > n {
|
||||
err = errors.New("hds: read buffer too small")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) WriteTo(w io.Writer) (int64, error) {
|
||||
var total int64
|
||||
for {
|
||||
b, err := c.read()
|
||||
if err != nil {
|
||||
return total, err
|
||||
}
|
||||
|
||||
n, err := w.Write(b)
|
||||
total += int64(n)
|
||||
if err != nil {
|
||||
return total, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Write(b []byte) (n int, err error) {
|
||||
n = len(b)
|
||||
|
||||
if n > 0xFFFFFF {
|
||||
return 0, errors.New("hds: write buffer too big")
|
||||
}
|
||||
|
||||
verify := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(verify, 0x01000000|uint32(n))
|
||||
if _, err = c.wr.Write(verify); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
nonce := make([]byte, hap.NonceSize)
|
||||
binary.LittleEndian.PutUint64(nonce, c.encryptCnt)
|
||||
c.encryptCnt++
|
||||
|
||||
buf := make([]byte, n+hap.Overhead)
|
||||
if _, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf[:0], nonce, b, verify); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = c.wr.Write(buf); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.wr.Flush()
|
||||
|
||||
c.send += n
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) LocalAddr() net.Addr {
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *Conn) RemoteAddr() net.Addr {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *Conn) SetDeadline(t time.Time) error {
|
||||
return c.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *Conn) SetReadDeadline(t time.Time) error {
|
||||
return c.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *Conn) SetWriteDeadline(t time.Time) error {
|
||||
return c.conn.SetWriteDeadline(t)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package hds
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEncryption(t *testing.T) {
|
||||
key := []byte(core.RandString(16, 0))
|
||||
salt := core.RandString(32, 0)
|
||||
|
||||
c, err := Client(nil, key, salt, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
c.wr = bufio.NewWriter(buf)
|
||||
|
||||
n, err := c.Write([]byte("test"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 4, n)
|
||||
|
||||
c, err = Client(nil, key, salt, false)
|
||||
c.rd = bufio.NewReader(buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
b := make([]byte, 32)
|
||||
n, err = c.Read(b)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "test", string(b[:n]))
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package hap
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
TXTConfigNumber = "c#" // Current configuration number (ex. 1, 2, 3)
|
||||
TXTDeviceID = "id" // Device ID of the accessory (ex. 77:75:87:A0:7D:F4)
|
||||
TXTModel = "md" // Model name of the accessory (ex. MJCTD02YL)
|
||||
TXTProtoVersion = "pv" // Protocol version string (ex. 1.1)
|
||||
TXTStateNumber = "s#" // Current state number (ex. 1)
|
||||
TXTCategory = "ci" // Accessory Category Identifier (ex. 2, 5, 17)
|
||||
TXTSetupHash = "sh" // Setup hash (ex. Y9w9hQ==)
|
||||
|
||||
// TXTFeatureFlags
|
||||
// - 0001b - Supports Apple Authentication Coprocessor
|
||||
// - 0010b - Supports Software Authentication
|
||||
TXTFeatureFlags = "ff" // Pairing Feature flags (ex. 0, 1, 2)
|
||||
|
||||
// TXTStatusFlags
|
||||
// - 0001b - Accessory has not been paired with any controllers
|
||||
// - 0100b - A problem has been detected on the accessory
|
||||
TXTStatusFlags = "sf" // Status flags (ex. 0, 1)
|
||||
|
||||
StatusPaired = "0"
|
||||
StatusNotPaired = "1"
|
||||
|
||||
CategoryBridge = "2"
|
||||
CategoryCamera = "17"
|
||||
CategoryDoorbell = "18"
|
||||
|
||||
StateM1 = 1
|
||||
StateM2 = 2
|
||||
StateM3 = 3
|
||||
StateM4 = 4
|
||||
StateM5 = 5
|
||||
StateM6 = 6
|
||||
|
||||
MethodPair = 0
|
||||
MethodPairMFi = 1 // if device has MFI cert
|
||||
MethodVerifyPair = 2
|
||||
MethodAddPairing = 3
|
||||
MethodDeletePairing = 4
|
||||
MethodListPairings = 5
|
||||
|
||||
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"`
|
||||
Status any `json:"status,omitempty"`
|
||||
Value any `json:"value,omitempty"`
|
||||
Event any `json:"ev,omitempty"`
|
||||
}
|
||||
|
||||
// 4.2.1.2 Invalid Setup Codes
|
||||
const insecurePINs = "00000000 11111111 22222222 33333333 44444444 55555555 66666666 77777777 88888888 99999999 12345678 87654321"
|
||||
|
||||
func SanitizePin(pin string) (string, error) {
|
||||
s := strings.ReplaceAll(pin, "-", "")
|
||||
if len(s) != 8 {
|
||||
return "", errors.New("hap: wrong PIN format: " + pin)
|
||||
}
|
||||
if strings.Contains(insecurePINs, s) {
|
||||
return "", errors.New("hap: insecure PIN: " + pin)
|
||||
}
|
||||
// 123-45-678
|
||||
return s[:3] + "-" + s[3:5] + "-" + s[5:], nil
|
||||
}
|
||||
|
||||
func GenerateKey() []byte {
|
||||
_, key, _ := ed25519.GenerateKey(nil)
|
||||
return key
|
||||
}
|
||||
|
||||
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:]
|
||||
}
|
||||
|
||||
func SetupHash(setupID, deviceID string) string {
|
||||
// should be setup_id (random 4 alphanum) + device_id (mac address)
|
||||
b := sha512.Sum512([]byte(setupID + deviceID))
|
||||
return base64.StdEncoding.EncodeToString(b[:4])
|
||||
}
|
||||
|
||||
func Append(items ...any) (b []byte) {
|
||||
for _, item := range items {
|
||||
switch v := item.(type) {
|
||||
case string:
|
||||
b = append(b, v...)
|
||||
case []byte:
|
||||
b = append(b, v[:]...)
|
||||
default:
|
||||
panic(v)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package hkdf
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
func Sha512(key []byte, salt, info string) ([]byte, error) {
|
||||
r := hkdf.New(sha512.New, key, []byte(salt), []byte(info))
|
||||
|
||||
buf := make([]byte, 32)
|
||||
_, err := io.ReadFull(r, buf)
|
||||
|
||||
return buf, err
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
package hap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/curve25519"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/ed25519"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
|
||||
"github.com/tadglines/go-pkgs/crypto/srp"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Pin string
|
||||
DeviceID string
|
||||
DevicePrivate []byte
|
||||
|
||||
// GetClientPublic may be nil, so client validation will be disabled
|
||||
GetClientPublic func(id string) []byte
|
||||
}
|
||||
|
||||
func (s *Server) ServerPublic() []byte {
|
||||
return s.DevicePrivate[32:]
|
||||
}
|
||||
|
||||
//func (s *Server) Status() string {
|
||||
// if len(s.Pairings) == 0 {
|
||||
// return StatusNotPaired
|
||||
// }
|
||||
// return StatusPaired
|
||||
//}
|
||||
|
||||
func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter) (id string, publicKey []byte, err error) {
|
||||
// STEP 1. Request from iPhone
|
||||
var plainM1 struct {
|
||||
State byte `tlv8:"6"`
|
||||
Method byte `tlv8:"0"`
|
||||
Flags uint32 `tlv8:"19"`
|
||||
}
|
||||
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil {
|
||||
return
|
||||
}
|
||||
if plainM1.State != StateM1 {
|
||||
err = newRequestError(plainM1)
|
||||
return
|
||||
}
|
||||
|
||||
username := []byte("Pair-Setup")
|
||||
|
||||
// Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE)
|
||||
pake, err := srp.NewSRP("rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pake.SaltLength = 16
|
||||
|
||||
salt, verifier, err := pake.ComputeVerifier([]byte(s.Pin))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
session := pake.NewServerSession(username, salt, verifier)
|
||||
|
||||
// STEP 2. Response to iPhone
|
||||
plainM2 := struct {
|
||||
State byte `tlv8:"6"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
Salt string `tlv8:"2"`
|
||||
}{
|
||||
State: StateM2,
|
||||
PublicKey: string(session.GetB()),
|
||||
Salt: string(salt),
|
||||
}
|
||||
body, err := tlv8.Marshal(plainM2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP 3. Request from iPhone
|
||||
if req, err = http.ReadRequest(rw.Reader); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var plainM3 struct {
|
||||
State byte `tlv8:"6"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
Proof string `tlv8:"4"`
|
||||
}
|
||||
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM3); err != nil {
|
||||
return
|
||||
}
|
||||
if plainM3.State != StateM3 {
|
||||
err = newRequestError(plainM3)
|
||||
return
|
||||
}
|
||||
|
||||
// important to compute key before verify client
|
||||
sessionShared, err := session.ComputeKey([]byte(plainM3.PublicKey))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !session.VerifyClientAuthenticator([]byte(plainM3.Proof)) {
|
||||
err = errors.New("hap: VerifyClientAuthenticator")
|
||||
return
|
||||
}
|
||||
|
||||
proof := session.ComputeAuthenticator([]byte(plainM3.Proof)) // server proof
|
||||
|
||||
// STEP 4. Response to iPhone
|
||||
payloadM4 := struct {
|
||||
State byte `tlv8:"6"`
|
||||
Proof string `tlv8:"4"`
|
||||
}{
|
||||
State: StateM4,
|
||||
Proof: string(proof),
|
||||
}
|
||||
if body, err = tlv8.Marshal(payloadM4); err != nil {
|
||||
return
|
||||
}
|
||||
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP 5. Request from iPhone
|
||||
if req, err = http.ReadRequest(rw.Reader); err != nil {
|
||||
return
|
||||
}
|
||||
var cipherM5 struct {
|
||||
State byte `tlv8:"6"`
|
||||
EncryptedData string `tlv8:"5"`
|
||||
}
|
||||
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM5); err != nil {
|
||||
return
|
||||
}
|
||||
if cipherM5.State != StateM5 {
|
||||
err = newRequestError(cipherM5)
|
||||
return
|
||||
}
|
||||
|
||||
// decrypt message using session shared
|
||||
encryptKey, err := hkdf.Sha512(sessionShared, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b, err := chacha20poly1305.Decrypt(encryptKey, "PS-Msg05", []byte(cipherM5.EncryptedData))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// unpack message from TLV8
|
||||
var plainM5 struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
Signature string `tlv8:"10"`
|
||||
}
|
||||
if err = tlv8.Unmarshal(b, &plainM5); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 3. verify client ID and Public
|
||||
remoteSign, err := hkdf.Sha512(
|
||||
sessionShared, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info",
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b = Append(remoteSign, plainM5.Identifier, plainM5.PublicKey)
|
||||
if !ed25519.ValidateSignature([]byte(plainM5.PublicKey), b, []byte(plainM5.Signature)) {
|
||||
err = errors.New("hap: ValidateSignature")
|
||||
return
|
||||
}
|
||||
|
||||
// 4. generate signature to our ID and Public
|
||||
localSign, err := hkdf.Sha512(
|
||||
sessionShared, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info",
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b = Append(localSign, s.DeviceID, s.ServerPublic()) // ServerPublic
|
||||
signature, err := ed25519.Signature(s.DevicePrivate, b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 5. pack our ID and Public
|
||||
plainM6 := struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
Signature string `tlv8:"10"`
|
||||
}{
|
||||
Identifier: s.DeviceID,
|
||||
PublicKey: string(s.ServerPublic()),
|
||||
Signature: string(signature),
|
||||
}
|
||||
if b, err = tlv8.Marshal(plainM6); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 6. encrypt message
|
||||
b, err = chacha20poly1305.Encrypt(encryptKey, "PS-Msg06", b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP 6. Response to iPhone
|
||||
cipherM6 := struct {
|
||||
State byte `tlv8:"6"`
|
||||
EncryptedData string `tlv8:"5"`
|
||||
}{
|
||||
State: StateM6,
|
||||
EncryptedData: string(b),
|
||||
}
|
||||
if body, err = tlv8.Marshal(cipherM6); err != nil {
|
||||
return
|
||||
}
|
||||
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
id = plainM5.Identifier
|
||||
publicKey = []byte(plainM5.PublicKey)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter) (id string, sessionKey []byte, err error) {
|
||||
// Request from iPhone
|
||||
var plainM1 struct {
|
||||
State byte `tlv8:"6"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
}
|
||||
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil {
|
||||
return
|
||||
}
|
||||
if plainM1.State != StateM1 {
|
||||
err = newRequestError(plainM1)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate the key pair
|
||||
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
|
||||
sessionShared, err := curve25519.SharedSecret(sessionPrivate, []byte(plainM1.PublicKey))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
encryptKey, err := hkdf.Sha512(
|
||||
sessionShared, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info",
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b := Append(sessionPublic, s.DeviceID, plainM1.PublicKey)
|
||||
signature, err := ed25519.Signature(s.DevicePrivate, b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M2. Response to iPhone
|
||||
plainM2 := struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
Signature string `tlv8:"10"`
|
||||
}{
|
||||
Identifier: s.DeviceID,
|
||||
Signature: string(signature),
|
||||
}
|
||||
if b, err = tlv8.Marshal(plainM2); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b, err = chacha20poly1305.Encrypt(encryptKey, "PV-Msg02", b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cipherM2 := struct {
|
||||
State byte `tlv8:"6"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
EncryptedData string `tlv8:"5"`
|
||||
}{
|
||||
State: StateM2,
|
||||
PublicKey: string(sessionPublic),
|
||||
EncryptedData: string(b),
|
||||
}
|
||||
body, err := tlv8.Marshal(cipherM2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M3. Request from iPhone
|
||||
if req, err = http.ReadRequest(rw.Reader); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var cipherM3 struct {
|
||||
State byte `tlv8:"6"`
|
||||
EncryptedData string `tlv8:"5"`
|
||||
}
|
||||
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM3); err != nil {
|
||||
return
|
||||
}
|
||||
if cipherM3.State != StateM3 {
|
||||
err = newRequestError(cipherM3)
|
||||
return
|
||||
}
|
||||
|
||||
b, err = chacha20poly1305.Decrypt(encryptKey, "PV-Msg03", []byte(cipherM3.EncryptedData))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var plainM3 struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
Signature string `tlv8:"10"`
|
||||
}
|
||||
if err = tlv8.Unmarshal(b, &plainM3); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if s.GetClientPublic != nil {
|
||||
clientPublic := s.GetClientPublic(plainM3.Identifier)
|
||||
if clientPublic == nil {
|
||||
err = errors.New("hap: PairVerify with unknown client_id: " + plainM3.Identifier)
|
||||
return
|
||||
}
|
||||
|
||||
b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic)
|
||||
if !ed25519.ValidateSignature(clientPublic, b, []byte(plainM3.Signature)) {
|
||||
err = errors.New("hap: ValidateSignature")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// STEP M4. Response to iPhone
|
||||
payloadM4 := struct {
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
State: StateM4,
|
||||
}
|
||||
if body, err = tlv8.Marshal(payloadM4); err != nil {
|
||||
return
|
||||
}
|
||||
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
id = plainM3.Identifier
|
||||
sessionKey = sessionShared
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func WriteResponse(w *bufio.Writer, statusCode int, contentType string, body []byte) error {
|
||||
header := fmt.Sprintf(
|
||||
"HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n",
|
||||
statusCode, http.StatusText(statusCode), contentType, len(body),
|
||||
)
|
||||
body = append([]byte(header), body...)
|
||||
if _, err := w.Write(body); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Flush()
|
||||
}
|
||||
|
||||
//func WriteBackoff(rw *bufio.ReadWriter) error {
|
||||
// plainM2 := struct {
|
||||
// State byte `tlv8:"6"`
|
||||
// Error byte `tlv8:"7"`
|
||||
// }{
|
||||
// State: StateM2,
|
||||
// Error: 3, // BackoffError
|
||||
// }
|
||||
// body, err := tlv8.Marshal(plainM2)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body)
|
||||
//}
|
||||
@@ -0,0 +1,32 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
FlagNFC = 1
|
||||
FlagIP = 2
|
||||
FlagBLE = 4
|
||||
FlagWAC = 8 // Wireless Accessory Configuration (WAC)/Apples MFi
|
||||
)
|
||||
|
||||
func GenerateSetupURI(category, pin, setupID string) string {
|
||||
c, _ := strconv.Atoi(category)
|
||||
p, _ := strconv.Atoi(strings.ReplaceAll(pin, "-", ""))
|
||||
payload := int64(c&0xFF)<<31 | int64(FlagIP&0xF)<<27 | int64(p&0x7FFFFFF)
|
||||
return "X-HM://" + FormatInt36(payload, 9) + setupID
|
||||
}
|
||||
|
||||
// FormatInt36 equal to strings.ToUpper(fmt.Sprintf("%0"+strconv.Itoa(n)+"s", strconv.FormatInt(value, 36)))
|
||||
func FormatInt36(value int64, n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := n - 1; 0 <= i; i-- {
|
||||
b[i] = digits[value%36]
|
||||
value /= 36
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
@@ -0,0 +1,18 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFormatAlphaNum(t *testing.T) {
|
||||
value := int64(999)
|
||||
n := 5
|
||||
s1 := strings.ToUpper(fmt.Sprintf("%0"+strconv.Itoa(n)+"s", strconv.FormatInt(value, 36)))
|
||||
s2 := FormatInt36(value, n)
|
||||
require.Equal(t, s1, s2)
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
package tlv8
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type errReader struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *errReader) Read([]byte) (int, error) {
|
||||
return 0, e.err
|
||||
}
|
||||
|
||||
func MarshalBase64(v any) (string, error) {
|
||||
b, err := Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func MarshalReader(v any) io.Reader {
|
||||
b, err := Marshal(v)
|
||||
if err != nil {
|
||||
return &errReader{err: err}
|
||||
}
|
||||
return bytes.NewReader(b)
|
||||
}
|
||||
|
||||
func Marshal(v any) ([]byte, error) {
|
||||
value := reflect.ValueOf(v)
|
||||
kind := value.Type().Kind()
|
||||
|
||||
if kind == reflect.Pointer {
|
||||
value = value.Elem()
|
||||
kind = value.Type().Kind()
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case reflect.Slice:
|
||||
return appendSlice(nil, value)
|
||||
case reflect.Struct:
|
||||
return appendStruct(nil, value)
|
||||
}
|
||||
|
||||
return nil, errors.New("tlv8: not implemented: " + kind.String())
|
||||
}
|
||||
|
||||
// separator the most confusing meaning in the documentation.
|
||||
// It can have a value of 0x00 or 0xFF or even 0x05.
|
||||
const separator = 0xFF
|
||||
|
||||
func appendSlice(b []byte, value reflect.Value) ([]byte, error) {
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
if i > 0 {
|
||||
b = append(b, separator, 0)
|
||||
}
|
||||
var err error
|
||||
if b, err = appendStruct(b, value.Index(i)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func appendStruct(b []byte, value reflect.Value) ([]byte, error) {
|
||||
valueType := value.Type()
|
||||
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
refField := value.Field(i)
|
||||
s, ok := valueType.Field(i).Tag.Lookup("tlv8")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
tag, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err = appendValue(b, byte(tag), refField)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) {
|
||||
var err error
|
||||
|
||||
switch value.Kind() {
|
||||
case reflect.Uint8:
|
||||
v := value.Uint()
|
||||
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
|
||||
|
||||
case reflect.Uint32:
|
||||
v := value.Uint()
|
||||
return append(b, tag, 4, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)), nil
|
||||
|
||||
case reflect.Uint64:
|
||||
v := value.Uint()
|
||||
return binary.LittleEndian.AppendUint64(append(b, tag, 8), v), nil
|
||||
|
||||
case reflect.Float32:
|
||||
v := math.Float32bits(float32(value.Float()))
|
||||
return append(b, tag, 4, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)), nil
|
||||
|
||||
case reflect.String:
|
||||
v := value.String()
|
||||
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:
|
||||
if value.Type().Elem().Kind() == reflect.Uint8 {
|
||||
n := value.Len()
|
||||
b = append(b, tag, byte(n))
|
||||
for i := 0; i < n; i++ {
|
||||
b = append(b, byte(value.Index(i).Uint()))
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
case reflect.Slice:
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
if i > 0 {
|
||||
b = append(b, separator, 0)
|
||||
}
|
||||
if b, err = appendValue(b, tag, value.Index(i)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
|
||||
case reflect.Struct:
|
||||
b = append(b, tag, 0)
|
||||
i := len(b)
|
||||
if b, err = appendStruct(b, value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b[i-1] = byte(len(b) - i) // set struct size
|
||||
return b, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("tlv8: not implemented: " + value.Kind().String())
|
||||
}
|
||||
|
||||
func UnmarshalBase64(in any, out any) error {
|
||||
s, _ := in.(string) // protect from in == nil
|
||||
data, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Unmarshal(data, out)
|
||||
}
|
||||
|
||||
func UnmarshalReader(r io.Reader, n int64, v any) error {
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
if n > 0 {
|
||||
data = make([]byte, n)
|
||||
_, err = io.ReadFull(r, data)
|
||||
} else {
|
||||
data, err = io.ReadAll(r)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func Unmarshal(data []byte, v any) error {
|
||||
if len(data) == 0 {
|
||||
return errors.New("tlv8: unmarshal zero data")
|
||||
}
|
||||
|
||||
value := reflect.ValueOf(v)
|
||||
kind := value.Kind()
|
||||
|
||||
if kind != reflect.Pointer {
|
||||
return errors.New("tlv8: value should be pointer: " + kind.String())
|
||||
}
|
||||
|
||||
value = value.Elem()
|
||||
kind = value.Kind()
|
||||
|
||||
if kind == reflect.Interface {
|
||||
value = value.Elem()
|
||||
kind = value.Kind()
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case reflect.Slice:
|
||||
return unmarshalSlice(data, value)
|
||||
case reflect.Struct:
|
||||
return unmarshalStruct(data, value)
|
||||
}
|
||||
|
||||
return errors.New("tlv8: not implemented: " + kind.String())
|
||||
}
|
||||
|
||||
// unmarshalTLV can return two types of errors:
|
||||
// - critical and then the value of []byte will be nil
|
||||
// - not critical and then []byte will contain the value
|
||||
func unmarshalTLV(b []byte, value reflect.Value) ([]byte, error) {
|
||||
if len(b) < 2 {
|
||||
return nil, errors.New("tlv8: wrong size: " + value.Type().Name())
|
||||
}
|
||||
|
||||
t := b[0]
|
||||
l := int(b[1])
|
||||
|
||||
// array item divider (t == 0x00 || t == 0xFF)
|
||||
if l == 0 {
|
||||
return b[2:], errors.New("tlv8: zero item")
|
||||
}
|
||||
|
||||
var v []byte
|
||||
|
||||
for {
|
||||
if len(b) < 2+l {
|
||||
return nil, errors.New("tlv8: wrong size: " + value.Type().Name())
|
||||
}
|
||||
|
||||
v = append(v, b[2:2+l]...)
|
||||
b = b[2+l:]
|
||||
|
||||
// if size == 255 and same tag - continue read big payload
|
||||
if l < 255 || len(b) < 2 || b[0] != t {
|
||||
break
|
||||
}
|
||||
|
||||
l = int(b[1])
|
||||
}
|
||||
|
||||
tag := strconv.Itoa(int(t))
|
||||
|
||||
valueField, ok := getStructField(value, tag)
|
||||
if !ok {
|
||||
return b, fmt.Errorf("tlv8: can't find T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name())
|
||||
}
|
||||
|
||||
if err := unmarshalValue(v, valueField); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func unmarshalSlice(b []byte, value reflect.Value) error {
|
||||
valueIndex := value.Index(growSlice(value))
|
||||
for len(b) > 0 {
|
||||
var err error
|
||||
if b, err = unmarshalTLV(b, valueIndex); err != nil {
|
||||
if b != nil {
|
||||
valueIndex = value.Index(growSlice(value))
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalStruct(b []byte, value reflect.Value) error {
|
||||
for len(b) > 0 {
|
||||
var err error
|
||||
if b, err = unmarshalTLV(b, value); b == nil && err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalValue(v []byte, value reflect.Value) error {
|
||||
switch value.Kind() {
|
||||
case reflect.Uint8:
|
||||
if len(v) != 1 {
|
||||
return errors.New("tlv8: wrong size: " + value.Type().Name())
|
||||
}
|
||||
value.SetUint(uint64(v[0]))
|
||||
|
||||
case reflect.Uint16:
|
||||
if len(v) != 2 {
|
||||
return errors.New("tlv8: wrong size: " + value.Type().Name())
|
||||
}
|
||||
value.SetUint(uint64(v[0]) | uint64(v[1])<<8)
|
||||
|
||||
case reflect.Uint32:
|
||||
if len(v) != 4 {
|
||||
return errors.New("tlv8: wrong size: " + value.Type().Name())
|
||||
}
|
||||
value.SetUint(uint64(v[0]) | uint64(v[1])<<8 | uint64(v[2])<<16 | uint64(v[3])<<24)
|
||||
|
||||
case reflect.Uint64:
|
||||
if len(v) != 8 {
|
||||
return errors.New("tlv8: wrong size: " + value.Type().Name())
|
||||
}
|
||||
value.SetUint(binary.LittleEndian.Uint64(v))
|
||||
|
||||
case reflect.Float32:
|
||||
f := math.Float32frombits(binary.LittleEndian.Uint32(v))
|
||||
value.SetFloat(float64(f))
|
||||
|
||||
case reflect.String:
|
||||
value.SetString(string(v))
|
||||
|
||||
case reflect.Array:
|
||||
if kind := value.Type().Elem().Kind(); kind != reflect.Uint8 {
|
||||
return errors.New("tlv8: unsupported array: " + kind.String())
|
||||
}
|
||||
|
||||
for i, b := range v {
|
||||
value.Index(i).SetUint(uint64(b))
|
||||
}
|
||||
return nil
|
||||
|
||||
case reflect.Slice:
|
||||
i := growSlice(value)
|
||||
return unmarshalValue(v, value.Index(i))
|
||||
|
||||
case reflect.Struct:
|
||||
return unmarshalStruct(v, value)
|
||||
|
||||
default:
|
||||
return errors.New("tlv8: not implemented: " + value.Kind().String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStructField(value reflect.Value, tag string) (reflect.Value, bool) {
|
||||
valueType := value.Type()
|
||||
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
valueField := value.Field(i)
|
||||
|
||||
if s, ok := valueType.Field(i).Tag.Lookup("tlv8"); ok && s == tag {
|
||||
return valueField, true
|
||||
}
|
||||
}
|
||||
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
|
||||
func growSlice(value reflect.Value) int {
|
||||
size := value.Len()
|
||||
|
||||
if size >= value.Cap() {
|
||||
newcap := value.Cap() + value.Cap()/2
|
||||
if newcap < 4 {
|
||||
newcap = 4
|
||||
}
|
||||
newValue := reflect.MakeSlice(value.Type(), value.Len(), newcap)
|
||||
reflect.Copy(newValue, value)
|
||||
value.Set(newValue)
|
||||
}
|
||||
|
||||
if size >= value.Len() {
|
||||
value.SetLen(size + 1)
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package tlv8
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMarshal(t *testing.T) {
|
||||
type Struct struct {
|
||||
Byte byte `tlv8:"1"`
|
||||
Uint16 uint16 `tlv8:"2"`
|
||||
Uint32 uint32 `tlv8:"3"`
|
||||
Float32 float32 `tlv8:"4"`
|
||||
String string `tlv8:"5"`
|
||||
Slice []byte `tlv8:"6"`
|
||||
Array [4]byte `tlv8:"7"`
|
||||
}
|
||||
|
||||
src := Struct{
|
||||
Byte: 1,
|
||||
Uint16: 2,
|
||||
Uint32: 3,
|
||||
Float32: 1.23,
|
||||
String: "123",
|
||||
Slice: []byte{1, 2, 3},
|
||||
Array: [4]byte{1, 2, 3, 4},
|
||||
}
|
||||
|
||||
b, err := Marshal(src)
|
||||
require.Nil(t, err)
|
||||
|
||||
var dst Struct
|
||||
err = Unmarshal(b, &dst)
|
||||
require.Nil(t, err)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func TestSlice1(t *testing.T) {
|
||||
var v struct {
|
||||
VideoAttrs []struct {
|
||||
Width uint16 `tlv8:"1"`
|
||||
Height uint16 `tlv8:"2"`
|
||||
Framerate uint8 `tlv8:"3"`
|
||||
} `tlv8:"3"`
|
||||
}
|
||||
|
||||
s := `030b010280070202380403011e ff00 030b010200050202d00203011e`
|
||||
b1, err := hex.DecodeString(strings.ReplaceAll(s, " ", ""))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = Unmarshal(b1, &v)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, v.VideoAttrs, 2)
|
||||
|
||||
b2, err := Marshal(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, b1, b2)
|
||||
}
|
||||
|
||||
func TestSlice2(t *testing.T) {
|
||||
var v []struct {
|
||||
Width uint16 `tlv8:"1"`
|
||||
Height uint16 `tlv8:"2"`
|
||||
Framerate uint8 `tlv8:"3"`
|
||||
}
|
||||
|
||||
s := `010280070202380403011e ff00 010200050202d00203011e`
|
||||
b1, err := hex.DecodeString(strings.ReplaceAll(s, " ", ""))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = Unmarshal(b1, &v)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, v, 2)
|
||||
|
||||
b2, err := Marshal(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, b1, b2)
|
||||
}
|
||||
Reference in New Issue
Block a user