@basmilius/apple-companion-link 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CODEOWNERS +1 -0
- package/LICENSE +21 -0
- package/dist/cli.d.ts +3 -0
- package/dist/const.d.ts +5 -0
- package/dist/crypto/chacha20.d.ts +7 -0
- package/dist/crypto/curve25519.d.ts +7 -0
- package/dist/crypto/hkdf.d.ts +8 -0
- package/dist/crypto/index.d.ts +3 -0
- package/dist/discovery/discovery.d.ts +10 -0
- package/dist/discovery/index.d.ts +1 -0
- package/dist/encoding/index.d.ts +3 -0
- package/dist/encoding/opack.d.ts +10 -0
- package/dist/encoding/plist.d.ts +2 -0
- package/dist/encoding/tlv8.d.ts +40 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +24 -0
- package/dist/protocol/api/companionLink.d.ts +71 -0
- package/dist/protocol/companionLink.d.ts +14 -0
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/pairing/companionLink.d.ts +24 -0
- package/dist/protocol/verify/companionLink.d.ts +18 -0
- package/dist/socket/base.d.ts +7 -0
- package/dist/socket/companionLink.d.ts +40 -0
- package/dist/socket/index.d.ts +2 -0
- package/package.json +56 -0
- package/src/cli.ts +19 -0
- package/src/const.ts +7 -0
- package/src/crypto/chacha20.ts +95 -0
- package/src/crypto/curve25519.ts +21 -0
- package/src/crypto/hkdf.ts +13 -0
- package/src/crypto/index.ts +13 -0
- package/src/discovery/discovery.ts +52 -0
- package/src/discovery/index.ts +1 -0
- package/src/encoding/index.ts +22 -0
- package/src/encoding/opack.ts +293 -0
- package/src/encoding/plist.ts +2 -0
- package/src/encoding/tlv8.ts +111 -0
- package/src/index.ts +3 -0
- package/src/net/getLocalIP.ts +25 -0
- package/src/net/getMacAddress.ts +25 -0
- package/src/net/index.ts +2 -0
- package/src/protocol/api/companionLink.ts +353 -0
- package/src/protocol/companionLink.ts +41 -0
- package/src/protocol/index.ts +1 -0
- package/src/protocol/pairing/companionLink.ts +286 -0
- package/src/protocol/verify/companionLink.ts +185 -0
- package/src/socket/base.ts +21 -0
- package/src/socket/companionLink.ts +249 -0
- package/src/socket/index.ts +2 -0
- package/src/test.ts +109 -0
- package/src/transient.ts +64 -0
- package/src/types.d.ts +40 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { CompanionLinkFrameType, type CompanionLinkSocket } from '@/socket';
|
|
2
|
+
import { SRP, SrpClient } from 'fast-srp-hap';
|
|
3
|
+
import { v4 as uuid } from 'uuid';
|
|
4
|
+
import tweetnacl from 'tweetnacl';
|
|
5
|
+
import { debug } from '@/cli';
|
|
6
|
+
import { AIRPLAY_TRANSIENT_PIN } from '@/const';
|
|
7
|
+
import { decryptChacha20, encryptChacha20, hkdf } from '@/crypto';
|
|
8
|
+
import { bailTlv, decodeTlv, encodeOPack, encodeTlv, TlvFlags, TlvMethod, TlvState, TlvValue } from '@/encoding';
|
|
9
|
+
import CompanionLink from '../companionLink';
|
|
10
|
+
|
|
11
|
+
export default class {
|
|
12
|
+
get name(): string {
|
|
13
|
+
return this.#name;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get pairingId(): Buffer {
|
|
17
|
+
return this.#pairingId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
readonly #name: string;
|
|
21
|
+
readonly #pairingId: Buffer;
|
|
22
|
+
readonly #protocol: CompanionLink;
|
|
23
|
+
readonly #socket: CompanionLinkSocket;
|
|
24
|
+
#publicKey: Buffer;
|
|
25
|
+
#secretKey: Buffer;
|
|
26
|
+
#srp: SrpClient;
|
|
27
|
+
|
|
28
|
+
constructor(protocol: CompanionLink, socket: CompanionLinkSocket) {
|
|
29
|
+
this.#protocol = protocol;
|
|
30
|
+
this.#socket = socket;
|
|
31
|
+
|
|
32
|
+
this.#name = 'Bas Companion Link';
|
|
33
|
+
this.#pairingId = Buffer.from(uuid().toUpperCase());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async start(): Promise<void> {
|
|
37
|
+
const keyPair = tweetnacl.sign.keyPair();
|
|
38
|
+
this.#publicKey = Buffer.from(keyPair.publicKey);
|
|
39
|
+
this.#secretKey = Buffer.from(keyPair.secretKey);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async pin(askPin: () => Promise<string>): Promise<PairingCredentials> {
|
|
43
|
+
const m1 = await this.#m1();
|
|
44
|
+
const m2 = await this.#m2(m1, await askPin());
|
|
45
|
+
const m3 = await this.#m3(m2);
|
|
46
|
+
const m4 = await this.#m4(m3);
|
|
47
|
+
const m5 = await this.#m5(m4);
|
|
48
|
+
const m6 = await this.#m6(m4, m5);
|
|
49
|
+
|
|
50
|
+
if (!m6) {
|
|
51
|
+
throw new Error('Pairing failed, could not get accessory keys.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return m6;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async transient(): Promise<TransientPairingCredentials> {
|
|
58
|
+
const m1 = await this.#m1([[TlvValue.Flags, TlvFlags.TransientPairing]]);
|
|
59
|
+
const m2 = await this.#m2(m1);
|
|
60
|
+
const m3 = await this.#m3(m2);
|
|
61
|
+
const m4 = await this.#m4(m3);
|
|
62
|
+
|
|
63
|
+
const accessoryToControllerKey = hkdf({
|
|
64
|
+
hash: 'sha512',
|
|
65
|
+
key: m4.sharedSecret,
|
|
66
|
+
length: 32,
|
|
67
|
+
salt: Buffer.from('Control-Salt'),
|
|
68
|
+
info: Buffer.from('Control-Read-Encryption-Key')
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const controllerToAccessoryKey = hkdf({
|
|
72
|
+
hash: 'sha512',
|
|
73
|
+
key: m4.sharedSecret,
|
|
74
|
+
length: 32,
|
|
75
|
+
salt: Buffer.from('Control-Salt'),
|
|
76
|
+
info: Buffer.from('Control-Write-Encryption-Key')
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
pairingId: this.#pairingId,
|
|
81
|
+
sharedSecret: m4.sharedSecret,
|
|
82
|
+
accessoryToControllerKey,
|
|
83
|
+
controllerToAccessoryKey
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async #m1(additionalTlv: [number, number | Buffer][] = []): Promise<M1> {
|
|
88
|
+
const [, response] = await this.#socket.exchange(CompanionLinkFrameType.PS_Start, {
|
|
89
|
+
_pd: encodeTlv([
|
|
90
|
+
[TlvValue.Method, TlvMethod.PairSetup],
|
|
91
|
+
[TlvValue.State, TlvState.M1],
|
|
92
|
+
...additionalTlv
|
|
93
|
+
]),
|
|
94
|
+
_pwTy: 1
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const data = this.#tlv(response);
|
|
98
|
+
const publicKey = data.get(TlvValue.PublicKey);
|
|
99
|
+
const salt = data.get(TlvValue.Salt);
|
|
100
|
+
|
|
101
|
+
return {publicKey, salt};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async #m2(m1: M1, pin: string = AIRPLAY_TRANSIENT_PIN): Promise<M2> {
|
|
105
|
+
const srpKey = await SRP.genKey(32);
|
|
106
|
+
|
|
107
|
+
this.#srp = new SrpClient(SRP.params.hap, m1.salt, Buffer.from('Pair-Setup'), Buffer.from(pin), srpKey, true);
|
|
108
|
+
this.#srp.setB(m1.publicKey);
|
|
109
|
+
|
|
110
|
+
const publicKey = this.#srp.computeA();
|
|
111
|
+
const proof = this.#srp.computeM1();
|
|
112
|
+
|
|
113
|
+
return {publicKey, proof};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async #m3(m2: M2): Promise<M3> {
|
|
117
|
+
const [, response] = await this.#socket.exchange(CompanionLinkFrameType.PS_Next, {
|
|
118
|
+
_pd: encodeTlv([
|
|
119
|
+
[TlvValue.State, TlvState.M3],
|
|
120
|
+
[TlvValue.PublicKey, m2.publicKey],
|
|
121
|
+
[TlvValue.Proof, m2.proof]
|
|
122
|
+
]),
|
|
123
|
+
_pwTy: 1
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const data = this.#tlv(response);
|
|
127
|
+
const serverProof = data.get(TlvValue.Proof);
|
|
128
|
+
|
|
129
|
+
return {serverProof};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async #m4(m3: M3): Promise<M4> {
|
|
133
|
+
this.#srp.checkM2(m3.serverProof);
|
|
134
|
+
|
|
135
|
+
const sharedSecret = this.#srp.computeK();
|
|
136
|
+
|
|
137
|
+
return {sharedSecret};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async #m5(m4: M4): Promise<M5> {
|
|
141
|
+
const iosDeviceX = hkdf({
|
|
142
|
+
hash: 'sha512',
|
|
143
|
+
key: m4.sharedSecret,
|
|
144
|
+
length: 32,
|
|
145
|
+
salt: Buffer.from('Pair-Setup-Controller-Sign-Salt', 'utf8'),
|
|
146
|
+
info: Buffer.from('Pair-Setup-Controller-Sign-Info', 'utf8')
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const sessionKey = hkdf({
|
|
150
|
+
hash: 'sha512',
|
|
151
|
+
key: m4.sharedSecret,
|
|
152
|
+
length: 32,
|
|
153
|
+
salt: Buffer.from('Pair-Setup-Encrypt-Salt', 'utf8'),
|
|
154
|
+
info: Buffer.from('Pair-Setup-Encrypt-Info', 'utf8')
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const deviceInfo = Buffer.concat([
|
|
158
|
+
iosDeviceX,
|
|
159
|
+
this.#pairingId,
|
|
160
|
+
this.#publicKey
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
const signature = tweetnacl.sign.detached(deviceInfo, this.#secretKey);
|
|
164
|
+
|
|
165
|
+
const innerTlv = encodeTlv([
|
|
166
|
+
[TlvValue.Identifier, this.#pairingId],
|
|
167
|
+
[TlvValue.PublicKey, this.#publicKey],
|
|
168
|
+
[TlvValue.Signature, Buffer.from(signature)],
|
|
169
|
+
[TlvValue.Name, Buffer.from(encodeOPack({
|
|
170
|
+
name: this.#name
|
|
171
|
+
}))]
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
const {authTag, ciphertext} = encryptChacha20(sessionKey, Buffer.from('PS-Msg05'), null, innerTlv);
|
|
175
|
+
const encrypted = Buffer.concat([ciphertext, authTag]);
|
|
176
|
+
|
|
177
|
+
const [, response] = await this.#socket.exchange(CompanionLinkFrameType.PS_Next, {
|
|
178
|
+
_pd: encodeTlv([
|
|
179
|
+
[TlvValue.State, TlvState.M5],
|
|
180
|
+
[TlvValue.EncryptedData, encrypted]
|
|
181
|
+
]),
|
|
182
|
+
_pwTy: 1
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const data = this.#tlv(response);
|
|
186
|
+
const encryptedDataRaw = data.get(TlvValue.EncryptedData);
|
|
187
|
+
const encryptedData = encryptedDataRaw.subarray(0, -16);
|
|
188
|
+
const encryptedTag = encryptedDataRaw.subarray(-16);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
authTag: encryptedTag,
|
|
192
|
+
data: encryptedData,
|
|
193
|
+
sessionKey
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async #m6(m4: M4, m5: M5): Promise<PairingCredentials> {
|
|
198
|
+
const data = decryptChacha20(m5.sessionKey, Buffer.from('PS-Msg06'), null, m5.data, m5.authTag);
|
|
199
|
+
const tlv = decodeTlv(data);
|
|
200
|
+
|
|
201
|
+
const accessoryIdentifier = tlv.get(TlvValue.Identifier);
|
|
202
|
+
const accessoryLongTermPublicKey = tlv.get(TlvValue.PublicKey);
|
|
203
|
+
const accessorySignature = tlv.get(TlvValue.Signature);
|
|
204
|
+
|
|
205
|
+
const accessoryX = hkdf({
|
|
206
|
+
hash: 'sha512',
|
|
207
|
+
key: m4.sharedSecret,
|
|
208
|
+
length: 32,
|
|
209
|
+
salt: Buffer.from('Pair-Setup-Accessory-Sign-Salt'),
|
|
210
|
+
info: Buffer.from('Pair-Setup-Accessory-Sign-Info')
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const accessoryInfo = Buffer.concat([
|
|
214
|
+
accessoryX,
|
|
215
|
+
accessoryIdentifier,
|
|
216
|
+
accessoryLongTermPublicKey
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
if (!tweetnacl.sign.detached.verify(accessoryInfo, accessorySignature, accessoryLongTermPublicKey)) {
|
|
220
|
+
throw new Error('Invalid accessory signature.');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
accessoryIdentifier: accessoryIdentifier.toString(),
|
|
225
|
+
accessoryLongTermPublicKey: accessoryLongTermPublicKey,
|
|
226
|
+
pairingId: this.#pairingId,
|
|
227
|
+
publicKey: this.#publicKey,
|
|
228
|
+
secretKey: this.#secretKey
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#tlv(response: unknown): Map<number, Buffer> {
|
|
233
|
+
if (typeof response !== 'object' || response === null) {
|
|
234
|
+
throw new Error('Invalid response from receiver.');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const data = decodeTlv(response['_pd']);
|
|
238
|
+
|
|
239
|
+
if (data.has(TlvValue.Error)) {
|
|
240
|
+
bailTlv(data);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
debug('Decoded TLV', data);
|
|
244
|
+
|
|
245
|
+
return data;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
type M1 = {
|
|
250
|
+
readonly publicKey: Buffer;
|
|
251
|
+
readonly salt: Buffer;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
type M2 = {
|
|
255
|
+
readonly publicKey: Buffer;
|
|
256
|
+
readonly proof: Buffer;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
type M3 = {
|
|
260
|
+
readonly serverProof: Buffer;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
type M4 = {
|
|
264
|
+
readonly sharedSecret: Buffer;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
type M5 = {
|
|
268
|
+
readonly authTag: Buffer;
|
|
269
|
+
readonly data: Buffer;
|
|
270
|
+
readonly sessionKey: Buffer;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
type PairingCredentials = {
|
|
274
|
+
readonly accessoryIdentifier: string;
|
|
275
|
+
readonly accessoryLongTermPublicKey: Buffer;
|
|
276
|
+
readonly pairingId: Buffer;
|
|
277
|
+
readonly publicKey: Buffer;
|
|
278
|
+
readonly secretKey: Buffer;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
type TransientPairingCredentials = {
|
|
282
|
+
readonly pairingId: Buffer;
|
|
283
|
+
readonly sharedSecret: Buffer;
|
|
284
|
+
readonly accessoryToControllerKey: Buffer;
|
|
285
|
+
readonly controllerToAccessoryKey: Buffer;
|
|
286
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import tweetnacl, { type BoxKeyPair } from 'tweetnacl';
|
|
2
|
+
import { debug } from '@/cli';
|
|
3
|
+
import { decryptChacha20, encryptChacha20, generateCurve25519KeyPair, generateCurve25519SharedSecKey, hkdf } from '@/crypto';
|
|
4
|
+
import { bailTlv, decodeTlv, encodeTlv, TlvState, TlvValue } from '@/encoding';
|
|
5
|
+
import { CompanionLinkFrameType, CompanionLinkSocket } from '@/socket';
|
|
6
|
+
import CompanionLink from '../companionLink';
|
|
7
|
+
|
|
8
|
+
export default class {
|
|
9
|
+
readonly #protocol: CompanionLink;
|
|
10
|
+
readonly #socket: CompanionLinkSocket;
|
|
11
|
+
readonly #ephemeralKeyPair: BoxKeyPair;
|
|
12
|
+
|
|
13
|
+
constructor(protocol: CompanionLink, socket: CompanionLinkSocket) {
|
|
14
|
+
this.#protocol = protocol;
|
|
15
|
+
this.#socket = socket;
|
|
16
|
+
this.#ephemeralKeyPair = generateCurve25519KeyPair();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async start(credentials: Credentials): Promise<AccessoryKeys> {
|
|
20
|
+
const m1 = await this.#m1();
|
|
21
|
+
const m2 = await this.#m2(credentials.accessoryIdentifier, credentials.accessoryLongTermPublicKey, m1);
|
|
22
|
+
|
|
23
|
+
await this.#m3(credentials.pairingId, credentials.secretKey, m2);
|
|
24
|
+
|
|
25
|
+
return await this.#m4(m2);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async #m1(): Promise<M1> {
|
|
29
|
+
const [, response] = await this.#socket.exchange(CompanionLinkFrameType.PV_Start, {
|
|
30
|
+
_pd: encodeTlv([
|
|
31
|
+
[TlvValue.State, TlvState.M1],
|
|
32
|
+
[TlvValue.PublicKey, Buffer.from(this.#ephemeralKeyPair.publicKey)]
|
|
33
|
+
]),
|
|
34
|
+
_auTy: 4
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const data = this.#tlv(response);
|
|
38
|
+
const serverPublicKey = data.get(TlvValue.PublicKey);
|
|
39
|
+
const encryptedData = data.get(TlvValue.EncryptedData);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
encryptedData,
|
|
43
|
+
serverPublicKey
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async #m2(localAccessoryIdentifier: string, longTermPublicKey: Buffer, m1: M1): Promise<M2> {
|
|
48
|
+
const sharedSecret = Buffer.from(generateCurve25519SharedSecKey(
|
|
49
|
+
this.#ephemeralKeyPair.secretKey,
|
|
50
|
+
m1.serverPublicKey
|
|
51
|
+
));
|
|
52
|
+
|
|
53
|
+
const sessionKey = hkdf({
|
|
54
|
+
hash: 'sha512',
|
|
55
|
+
key: sharedSecret,
|
|
56
|
+
length: 32,
|
|
57
|
+
salt: Buffer.from('Pair-Verify-Encrypt-Salt'),
|
|
58
|
+
info: Buffer.from('Pair-Verify-Encrypt-Info')
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const encryptedData = m1.encryptedData.subarray(0, -16);
|
|
62
|
+
const encryptedTag = m1.encryptedData.subarray(-16);
|
|
63
|
+
|
|
64
|
+
const data = decryptChacha20(sessionKey, Buffer.from('PV-Msg02'), null, encryptedData, encryptedTag);
|
|
65
|
+
const tlv = decodeTlv(data);
|
|
66
|
+
|
|
67
|
+
const accessoryIdentifier = tlv.get(TlvValue.Identifier);
|
|
68
|
+
const accessorySignature = tlv.get(TlvValue.Signature);
|
|
69
|
+
|
|
70
|
+
if (accessoryIdentifier.toString() !== localAccessoryIdentifier) {
|
|
71
|
+
throw new Error(`Invalid accessory identifier. Expected ${accessoryIdentifier.toString()} to be ${localAccessoryIdentifier}.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const accessoryInfo = Buffer.concat([
|
|
75
|
+
m1.serverPublicKey,
|
|
76
|
+
accessoryIdentifier,
|
|
77
|
+
this.#ephemeralKeyPair.publicKey
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
if (!tweetnacl.sign.detached.verify(accessoryInfo, accessorySignature, longTermPublicKey)) {
|
|
81
|
+
throw new Error('Invalid accessory signature.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
serverEphemeralPublicKey: m1.serverPublicKey,
|
|
86
|
+
sessionKey,
|
|
87
|
+
sharedSecret
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async #m3(pairingId: Buffer, secretKey: Buffer, m2: M2): Promise<M3> {
|
|
92
|
+
const iosDeviceInfo = Buffer.concat([
|
|
93
|
+
this.#ephemeralKeyPair.publicKey,
|
|
94
|
+
pairingId,
|
|
95
|
+
m2.serverEphemeralPublicKey
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
const iosDeviceSignature = Buffer.from(tweetnacl.sign.detached(iosDeviceInfo, secretKey));
|
|
99
|
+
|
|
100
|
+
const innerTlv = encodeTlv([
|
|
101
|
+
[TlvValue.Identifier, pairingId],
|
|
102
|
+
[TlvValue.Signature, iosDeviceSignature]
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
const {authTag, ciphertext} = encryptChacha20(m2.sessionKey, Buffer.from('PV-Msg03'), null, innerTlv);
|
|
106
|
+
const encrypted = Buffer.concat([ciphertext, authTag]);
|
|
107
|
+
|
|
108
|
+
const [, response] = await this.#socket.exchange(CompanionLinkFrameType.PV_Next, {
|
|
109
|
+
_pd: encodeTlv([
|
|
110
|
+
[TlvValue.State, TlvState.M3],
|
|
111
|
+
[TlvValue.EncryptedData, encrypted]
|
|
112
|
+
]),
|
|
113
|
+
_auTy: 4
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log(this.#tlv(response));
|
|
117
|
+
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async #m4(m2: M2): Promise<AccessoryKeys> {
|
|
122
|
+
const accessoryToControllerKey = hkdf({
|
|
123
|
+
hash: 'sha512',
|
|
124
|
+
key: m2.sharedSecret,
|
|
125
|
+
length: 32,
|
|
126
|
+
salt: Buffer.alloc(0),
|
|
127
|
+
info: Buffer.from('ServerEncrypt-main')
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const controllerToAccessoryKey = hkdf({
|
|
131
|
+
hash: 'sha512',
|
|
132
|
+
key: m2.sharedSecret,
|
|
133
|
+
length: 32,
|
|
134
|
+
salt: Buffer.alloc(0),
|
|
135
|
+
info: Buffer.from('ClientEncrypt-main')
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
accessoryToControllerKey,
|
|
140
|
+
controllerToAccessoryKey
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#tlv(response: unknown): Map<number, Buffer> {
|
|
145
|
+
if (typeof response !== 'object' || response === null) {
|
|
146
|
+
throw new Error('Invalid response from receiver.');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const data = decodeTlv(response['_pd']);
|
|
150
|
+
|
|
151
|
+
if (data.has(TlvValue.Error)) {
|
|
152
|
+
bailTlv(data);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
debug('Decoded TLV', data);
|
|
156
|
+
|
|
157
|
+
return data;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
type Credentials = {
|
|
162
|
+
readonly accessoryIdentifier: string;
|
|
163
|
+
readonly accessoryLongTermPublicKey: Buffer;
|
|
164
|
+
readonly pairingId: Buffer;
|
|
165
|
+
readonly publicKey: Buffer;
|
|
166
|
+
readonly secretKey: Buffer;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
type M1 = {
|
|
170
|
+
readonly encryptedData: Buffer;
|
|
171
|
+
readonly serverPublicKey: Buffer;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
type M2 = {
|
|
175
|
+
readonly serverEphemeralPublicKey: Buffer;
|
|
176
|
+
readonly sessionKey: Buffer;
|
|
177
|
+
readonly sharedSecret: Buffer;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
type M3 = {};
|
|
181
|
+
|
|
182
|
+
type AccessoryKeys = {
|
|
183
|
+
readonly accessoryToControllerKey: Buffer;
|
|
184
|
+
readonly controllerToAccessoryKey: Buffer;
|
|
185
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default class BaseSocket extends EventTarget {
|
|
2
|
+
get address(): string {
|
|
3
|
+
return this.#address;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
get port(): number {
|
|
7
|
+
return this.#port;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
readonly #address: string;
|
|
11
|
+
readonly #port: number;
|
|
12
|
+
|
|
13
|
+
constructor(address: string, port: number) {
|
|
14
|
+
super();
|
|
15
|
+
this.#address = address;
|
|
16
|
+
this.#port = port;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async connect(): Promise<void> {
|
|
20
|
+
}
|
|
21
|
+
}
|