install go2rtc on bob

This commit is contained in:
2026-04-04 19:36:14 +02:00
parent f0b56e63d1
commit ccf88187b8
537 changed files with 69213 additions and 0 deletions
+54
View File
@@ -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)
}
+375
View File
@@ -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)
}
}
+173
View File
@@ -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
}
+176
View File
@@ -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]))
}
+130
View File
@@ -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
}
+396
View File
@@ -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)
}