@basmilius/apple-companion-link 0.0.2 → 0.0.4
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/dist/index.js +3 -3
- package/dist/index.js.map +4 -4
- package/dist/socket/companionLink.d.ts +1 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +5 -0
- package/dist/test.js.map +25 -0
- package/package.json +1 -2
- package/src/cli.ts +0 -19
- package/src/const.ts +0 -7
- package/src/crypto/chacha20.ts +0 -95
- package/src/crypto/curve25519.ts +0 -21
- package/src/crypto/hkdf.ts +0 -13
- package/src/crypto/index.ts +0 -13
- package/src/discovery/discovery.ts +0 -52
- package/src/discovery/index.ts +0 -1
- package/src/encoding/index.ts +0 -22
- package/src/encoding/opack.ts +0 -293
- package/src/encoding/plist.ts +0 -2
- package/src/encoding/tlv8.ts +0 -111
- package/src/index.ts +0 -3
- package/src/net/getLocalIP.ts +0 -25
- package/src/net/getMacAddress.ts +0 -25
- package/src/net/index.ts +0 -2
- package/src/protocol/api/companionLink.ts +0 -353
- package/src/protocol/companionLink.ts +0 -41
- package/src/protocol/index.ts +0 -1
- package/src/protocol/pairing/companionLink.ts +0 -286
- package/src/protocol/verify/companionLink.ts +0 -185
- package/src/socket/base.ts +0 -21
- package/src/socket/companionLink.ts +0 -249
- package/src/socket/index.ts +0 -2
- package/src/test.ts +0 -109
- package/src/transient.ts +0 -64
- package/src/types.d.ts +0 -40
|
@@ -1,185 +0,0 @@
|
|
|
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
|
-
};
|
package/src/socket/base.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import { randomInt } from 'node:crypto';
|
|
2
|
-
import { Socket } from 'node:net';
|
|
3
|
-
import { debug } from '@/cli';
|
|
4
|
-
import { decryptChacha20, encryptChacha20 } from '@/crypto';
|
|
5
|
-
import { decodeOPack, encodeOPack, opackSizedInt } from '@/encoding';
|
|
6
|
-
import BaseSocket from './base';
|
|
7
|
-
|
|
8
|
-
export default class extends BaseSocket {
|
|
9
|
-
get isEncrypted(): boolean {
|
|
10
|
-
return !!this.#readKey && !!this.#writeKey;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
readonly #socket: Socket;
|
|
14
|
-
readonly #queue: Record<number, Function> = {};
|
|
15
|
-
#buffer: Buffer = Buffer.alloc(0);
|
|
16
|
-
#readCount: number;
|
|
17
|
-
#readKey?: Buffer;
|
|
18
|
-
#writeCount: number;
|
|
19
|
-
#writeKey?: Buffer;
|
|
20
|
-
#xid: number;
|
|
21
|
-
|
|
22
|
-
constructor(address: string, port: number) {
|
|
23
|
-
super(address, port);
|
|
24
|
-
|
|
25
|
-
this.#xid = randomInt(0, 2 ** 16);
|
|
26
|
-
|
|
27
|
-
this.onClose = this.onClose.bind(this);
|
|
28
|
-
this.onConnect = this.onConnect.bind(this);
|
|
29
|
-
this.onData = this.onData.bind(this);
|
|
30
|
-
this.onEnd = this.onEnd.bind(this);
|
|
31
|
-
this.onError = this.onError.bind(this);
|
|
32
|
-
|
|
33
|
-
this.#socket = new Socket();
|
|
34
|
-
this.#socket.on('close', this.onClose);
|
|
35
|
-
this.#socket.on('connect', this.onConnect);
|
|
36
|
-
this.#socket.on('data', this.onData);
|
|
37
|
-
this.#socket.on('end', this.onEnd);
|
|
38
|
-
this.#socket.on('error', this.onError);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async connect(): Promise<void> {
|
|
42
|
-
debug(`Connecting to ${this.address}:${this.port}...`);
|
|
43
|
-
|
|
44
|
-
return await new Promise(resolve => {
|
|
45
|
-
this.#socket.connect({
|
|
46
|
-
host: this.address,
|
|
47
|
-
port: this.port,
|
|
48
|
-
keepAlive: true
|
|
49
|
-
}, resolve);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async enableEncryption(readKey: Buffer, writeKey: Buffer): Promise<void> {
|
|
54
|
-
this.#readKey = readKey;
|
|
55
|
-
this.#writeKey = writeKey;
|
|
56
|
-
this.#readCount = 0;
|
|
57
|
-
this.#writeCount = 0;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async exchange(type: number, obj: Record<string, unknown>): Promise<[number, unknown]> {
|
|
61
|
-
const _x = this.#xid;
|
|
62
|
-
|
|
63
|
-
return new Promise<[number, number]>((resolve, reject) => {
|
|
64
|
-
if (PairFrameTypes.includes(type)) {
|
|
65
|
-
this.#queue[-1] = resolve;
|
|
66
|
-
} else {
|
|
67
|
-
this.#queue[_x] = resolve;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
this.send(type, obj).catch(reject);
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async send(type: number, obj: Record<string, unknown>): Promise<void> {
|
|
75
|
-
const _x = this.#xid++;
|
|
76
|
-
obj._x ??= opackSizedInt(_x, 8);
|
|
77
|
-
|
|
78
|
-
let payload = Buffer.from(encodeOPack(obj));
|
|
79
|
-
let payloadLength = payload.byteLength;
|
|
80
|
-
|
|
81
|
-
if (this.isEncrypted && payloadLength > 0) {
|
|
82
|
-
payloadLength += 16;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const header = Buffer.alloc(4);
|
|
86
|
-
header.writeUint8(type, 0);
|
|
87
|
-
header.writeUintBE(payloadLength, 1, 3);
|
|
88
|
-
|
|
89
|
-
let data: Buffer;
|
|
90
|
-
|
|
91
|
-
if (this.isEncrypted) {
|
|
92
|
-
const nonce = Buffer.alloc(12);
|
|
93
|
-
nonce.writeBigUInt64LE(BigInt(this.#writeCount++), 0);
|
|
94
|
-
|
|
95
|
-
const encrypted = encryptChacha20(this.#writeKey, nonce, header, payload);
|
|
96
|
-
data = Buffer.concat([header, encrypted.ciphertext, encrypted.authTag]);
|
|
97
|
-
} else {
|
|
98
|
-
data = Buffer.concat([header, payload]);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
debug('Send data frame', this.isEncrypted, Buffer.from(data).toString('hex'), obj);
|
|
102
|
-
|
|
103
|
-
return new Promise((resolve, reject) => {
|
|
104
|
-
this.#socket.write(data, err => err && reject(err));
|
|
105
|
-
resolve();
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async onClose(): Promise<void> {
|
|
110
|
-
debug(`Connection closed from ${this.address}:${this.port}`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async onConnect(): Promise<void> {
|
|
114
|
-
debug(`Connected to ${this.address}:${this.port}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async onData(buffer: Buffer): Promise<void> {
|
|
118
|
-
// debug('Received data frame', buffer.toString('hex'));
|
|
119
|
-
|
|
120
|
-
this.#buffer = Buffer.concat([this.#buffer, buffer]);
|
|
121
|
-
|
|
122
|
-
while (this.#buffer.byteLength >= 4) {
|
|
123
|
-
const header = this.#buffer.subarray(0, 4);
|
|
124
|
-
const payloadLength = header.readUintBE(1, 3);
|
|
125
|
-
|
|
126
|
-
if (this.#buffer.byteLength < payloadLength) {
|
|
127
|
-
debug('Not enough data yet, waiting on the next frame..');
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
let data = this.#buffer.subarray(0, 4 + payloadLength);
|
|
132
|
-
data = await this.#decrypt(data);
|
|
133
|
-
|
|
134
|
-
this.#buffer = this.#buffer.subarray(data.byteLength);
|
|
135
|
-
|
|
136
|
-
let payload = data.subarray(4);
|
|
137
|
-
|
|
138
|
-
if (OPackFrameTypes.includes(header.readInt8())) {
|
|
139
|
-
[payload] = decodeOPack(payload);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
debug('Decoded OPACK', {header, payload});
|
|
143
|
-
|
|
144
|
-
if ('_x' in payload) {
|
|
145
|
-
const _x = (payload as any)._x;
|
|
146
|
-
|
|
147
|
-
if (_x in this.#queue) {
|
|
148
|
-
const resolve = this.#queue[_x] ?? null;
|
|
149
|
-
resolve?.([header, payload]);
|
|
150
|
-
|
|
151
|
-
delete this.#queue[_x];
|
|
152
|
-
} else {
|
|
153
|
-
// probably an event
|
|
154
|
-
const content = payload['_c'];
|
|
155
|
-
const keys = Object.keys(content).map(k => k.substring(0, -3));
|
|
156
|
-
|
|
157
|
-
for (const key of keys) {
|
|
158
|
-
this.dispatchEvent(new CustomEvent(key, {detail: content[key]}));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} else if (this.#queue[-1]) {
|
|
162
|
-
const _x = -1;
|
|
163
|
-
const resolve = this.#queue[_x] ?? null;
|
|
164
|
-
resolve?.([header, payload]);
|
|
165
|
-
|
|
166
|
-
delete this.#queue[_x];
|
|
167
|
-
} else {
|
|
168
|
-
debug('No handler for message', [header, payload]);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async onEnd(): Promise<void> {
|
|
174
|
-
debug('Connection ended');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async onError(err: Error): Promise<void> {
|
|
178
|
-
debug('Error received', err);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async #decrypt(data: Buffer): Promise<Buffer> {
|
|
182
|
-
if (!this.isEncrypted) {
|
|
183
|
-
return data;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const header = data.subarray(0, 4);
|
|
187
|
-
const payloadLength = header.readUintBE(1, 3);
|
|
188
|
-
|
|
189
|
-
const payload = data.subarray(4, payloadLength + 16);
|
|
190
|
-
const authTag = payload.subarray(payload.byteLength - 16);
|
|
191
|
-
const ciphertext = payload.subarray(0, payload.byteLength - 16);
|
|
192
|
-
|
|
193
|
-
const nonce = Buffer.alloc(12);
|
|
194
|
-
nonce.writeBigUint64LE(BigInt(this.#readCount++), 0);
|
|
195
|
-
|
|
196
|
-
const decrypted = decryptChacha20(this.#readKey, nonce, header, ciphertext, authTag);
|
|
197
|
-
|
|
198
|
-
return Buffer.concat([header, decrypted, authTag]);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export const FrameType = {
|
|
203
|
-
Unknown: 0,
|
|
204
|
-
Noop: 1,
|
|
205
|
-
|
|
206
|
-
PS_Start: 3,
|
|
207
|
-
PS_Next: 4,
|
|
208
|
-
PV_Start: 5,
|
|
209
|
-
PV_Next: 6,
|
|
210
|
-
|
|
211
|
-
U_OPACK: 7,
|
|
212
|
-
E_OPACK: 8,
|
|
213
|
-
P_OPACK: 9,
|
|
214
|
-
|
|
215
|
-
PA_Request: 10,
|
|
216
|
-
PA_Response: 11,
|
|
217
|
-
|
|
218
|
-
SessionStartRequest: 16,
|
|
219
|
-
SessionStartResponse: 17,
|
|
220
|
-
SessionData: 18,
|
|
221
|
-
|
|
222
|
-
FamilyIdentityRequest: 32,
|
|
223
|
-
FamilyIdentityResponse: 33,
|
|
224
|
-
FamilyIdentityUpdate: 34
|
|
225
|
-
} as const;
|
|
226
|
-
|
|
227
|
-
export const MessageType = {
|
|
228
|
-
Event: 1,
|
|
229
|
-
Request: 2,
|
|
230
|
-
Response: 3
|
|
231
|
-
} as const;
|
|
232
|
-
|
|
233
|
-
export const OPackFrameTypes: number[] = [
|
|
234
|
-
FrameType.PS_Start,
|
|
235
|
-
FrameType.PS_Next,
|
|
236
|
-
FrameType.PV_Start,
|
|
237
|
-
FrameType.PV_Next,
|
|
238
|
-
|
|
239
|
-
FrameType.U_OPACK,
|
|
240
|
-
FrameType.E_OPACK,
|
|
241
|
-
FrameType.P_OPACK
|
|
242
|
-
];
|
|
243
|
-
|
|
244
|
-
const PairFrameTypes: number[] = [
|
|
245
|
-
FrameType.PS_Start,
|
|
246
|
-
FrameType.PS_Next,
|
|
247
|
-
FrameType.PV_Start,
|
|
248
|
-
FrameType.PV_Next
|
|
249
|
-
];
|
package/src/socket/index.ts
DELETED
package/src/test.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { write } from 'bun';
|
|
2
|
-
import { debug, prompt, waitFor } from '@/cli';
|
|
3
|
-
import { Discovery } from '@/discovery';
|
|
4
|
-
import { parseBinaryPlist } from '@/encoding';
|
|
5
|
-
import { CompanionLink } from '@/protocol';
|
|
6
|
-
|
|
7
|
-
const discovery = Discovery.companionLink();
|
|
8
|
-
const device = await discovery.findUntil('Woonkamer TV._companion-link._tcp.local');
|
|
9
|
-
|
|
10
|
-
const protocol = new CompanionLink(device);
|
|
11
|
-
await protocol.connect();
|
|
12
|
-
|
|
13
|
-
async function pair(): Promise<void> {
|
|
14
|
-
await protocol.pairing.start();
|
|
15
|
-
const credentials = await protocol.pairing.pin(async () => await prompt('Enter PIN'));
|
|
16
|
-
|
|
17
|
-
console.log({
|
|
18
|
-
accessoryIdentifier: credentials.accessoryIdentifier,
|
|
19
|
-
accessoryLongTermPublicKey: credentials.accessoryLongTermPublicKey.toString('hex'),
|
|
20
|
-
pairingId: credentials.pairingId.toString('hex'),
|
|
21
|
-
publicKey: credentials.publicKey.toString('hex'),
|
|
22
|
-
secretKey: credentials.secretKey.toString('hex')
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function verify(): Promise<void> {
|
|
27
|
-
const credentials = {
|
|
28
|
-
accessoryIdentifier: '7EEEA518-06CC-486C-A8B8-4A07CDBE6267',
|
|
29
|
-
accessoryLongTermPublicKey: Buffer.from('cfb3fb0e0eb494d9058d5051c94400b35251e3faad66542b9551a1496570628d', 'hex'),
|
|
30
|
-
pairingId: Buffer.from('31343733314638332d334438422d344242362d393530302d433544464243383737443735', 'hex'),
|
|
31
|
-
publicKey: Buffer.from('4b7707b544e9c3b7a3609d0c9f7c9f013a71b415568bff597013d65e14d88918', 'hex'),
|
|
32
|
-
secretKey: Buffer.from('3933f2526014609fc437d2b0c7968b476f3ee0fd2d836fc5e97971fbd26adae04b7707b544e9c3b7a3609d0c9f7c9f013a71b415568bff597013d65e14d88918', 'hex')
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const keys = await protocol.verify.start(credentials);
|
|
36
|
-
|
|
37
|
-
await waitFor(250);
|
|
38
|
-
|
|
39
|
-
await protocol.socket.enableEncryption(
|
|
40
|
-
keys.accessoryToControllerKey,
|
|
41
|
-
keys.controllerToAccessoryKey
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
await protocol.api._systemInfo(credentials.pairingId);
|
|
45
|
-
await protocol.api._touchStart();
|
|
46
|
-
await protocol.api._sessionStart();
|
|
47
|
-
await protocol.api._tvrcSessionStart();
|
|
48
|
-
|
|
49
|
-
await protocol.api._unsubscribe('_iMC');
|
|
50
|
-
await protocol.api._subscribe('TVSystemStatus', (e: CustomEvent) => debug(e));
|
|
51
|
-
|
|
52
|
-
// await protocol.api._subscribe('NowPlayingInfo', handleNowPlayingInfo);
|
|
53
|
-
// await protocol.api.fetchNowPlayingInfo();
|
|
54
|
-
|
|
55
|
-
// await protocol.api._subscribe('SupportedActions', (e: CustomEvent) => debug(e));
|
|
56
|
-
// await protocol.api.fetchSupportedActions();
|
|
57
|
-
|
|
58
|
-
// await protocol.api._subscribe('PushSiriRemoteInfo', (e: CustomEvent) => debug(e));
|
|
59
|
-
// const data = await protocol.api.getSiriRemoteInfo();
|
|
60
|
-
// debug(data);
|
|
61
|
-
|
|
62
|
-
// await protocol.api._subscribe('MediaControlStatus', (e: CustomEvent) => debug(e));
|
|
63
|
-
// const data = await protocol.api.fetchMediaControlStatus();
|
|
64
|
-
// debug(data);
|
|
65
|
-
|
|
66
|
-
// debug('Attention state', await protocol.api.getAttentionState());
|
|
67
|
-
// debug('Launchable apps', await protocol.api.getLaunchableApps());
|
|
68
|
-
// debug('Available user accounts', await protocol.api.getUserAccounts());
|
|
69
|
-
|
|
70
|
-
// await protocol.api.launchApp('com.apple.TVMusic');
|
|
71
|
-
// await protocol.api.launchUrl('nflx://www.netflix.com/title/70291117');
|
|
72
|
-
// await protocol.api.switchUserAccount('71A6CA15-5268-4820-9DD8-1C53F980C149');
|
|
73
|
-
|
|
74
|
-
// await protocol.api.pressButton('Select');
|
|
75
|
-
// await protocol.api.pressButton('VolumeDown');
|
|
76
|
-
|
|
77
|
-
// await protocol.api.mediaControlCommand('Pause');
|
|
78
|
-
// await waitFor(2000);
|
|
79
|
-
// await protocol.api.mediaControlCommand('Play');
|
|
80
|
-
|
|
81
|
-
// await protocol.api.pressButton('PageUp');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function handleNowPlayingInfo(evt: CustomEvent): Promise<void> {
|
|
85
|
-
const {detail: nowPlaying} = evt;
|
|
86
|
-
const parsed = parseBinaryPlist(Buffer.from(nowPlaying).buffer) as any;
|
|
87
|
-
|
|
88
|
-
if (!parsed.$objects[15]) {
|
|
89
|
-
try {
|
|
90
|
-
await write('./artwork.png', parsed.$objects[14]);
|
|
91
|
-
} catch (_) {
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (parsed.$objects[4]) {
|
|
96
|
-
debug(`Now playing ${parsed.$objects[8]} on Apple TV.`);
|
|
97
|
-
} else {
|
|
98
|
-
debug('Not playing?');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// debug(parsed);
|
|
102
|
-
// debug('Keys', parsed.$objects[1]);
|
|
103
|
-
// debug('Image data is placeholder', parsed.$objects[15]);
|
|
104
|
-
// debug('metadata', parsed.$objects[6]);
|
|
105
|
-
// debug('playback state', parsed.$objects[4]);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// await pair();
|
|
109
|
-
await verify();
|
package/src/transient.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { randomInt } from 'node:crypto';
|
|
2
|
-
import { Discovery } from '@/discovery';
|
|
3
|
-
import { CompanionLink } from '@/protocol';
|
|
4
|
-
import { CompanionLinkFrameType, CompanionLinkMessageType } from '@/socket';
|
|
5
|
-
|
|
6
|
-
const discovery = Discovery.companionLink();
|
|
7
|
-
const device = await discovery.findUntil('Woonkamer HomePod (3)._companion-link._tcp.local');
|
|
8
|
-
|
|
9
|
-
const protocol = new CompanionLink(device);
|
|
10
|
-
await protocol.connect();
|
|
11
|
-
|
|
12
|
-
async function pair(): Promise<void> {
|
|
13
|
-
await protocol.pairing.start();
|
|
14
|
-
const keys = await protocol.pairing.transient();
|
|
15
|
-
|
|
16
|
-
await protocol.socket.enableEncryption(
|
|
17
|
-
keys.accessoryToControllerKey,
|
|
18
|
-
keys.controllerToAccessoryKey
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
const [, _ss] = await protocol.socket.sendLRCP(CompanionLinkFrameType.E_OPACK, {
|
|
22
|
-
_i: '_sessionStart',
|
|
23
|
-
_t: CompanionLinkMessageType.Request,
|
|
24
|
-
_c: {
|
|
25
|
-
_srvT: 'com.apple.tvremoteservices',
|
|
26
|
-
_sid: randomInt(0, 2 ** 32 - 1)
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
console.log(_ss);
|
|
31
|
-
|
|
32
|
-
const [, _si] = await protocol.socket.sendLRCP(CompanionLinkFrameType.E_OPACK, {
|
|
33
|
-
_i: '_systemInfo',
|
|
34
|
-
_t: CompanionLinkMessageType.Request,
|
|
35
|
-
_c: {
|
|
36
|
-
_bf: 0,
|
|
37
|
-
_cf: 512,
|
|
38
|
-
_clFl: 128,
|
|
39
|
-
_i: '',
|
|
40
|
-
_idsID: keys.pairingId.toString('hex'),
|
|
41
|
-
_pubID: 'FF:70:79:61:74:76',
|
|
42
|
-
_sf: 256,
|
|
43
|
-
_sv: '170.18',
|
|
44
|
-
model: 'iPhone10,6',
|
|
45
|
-
nmae: 'iPhone van Bas'
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
console.log(_si);
|
|
50
|
-
|
|
51
|
-
const [, _ts] = await protocol.socket.send(CompanionLinkFrameType.E_OPACK, {
|
|
52
|
-
_i: '_touchStart',
|
|
53
|
-
_t: CompanionLinkMessageType.Request,
|
|
54
|
-
_c: {
|
|
55
|
-
_height: 1000.0,
|
|
56
|
-
_width: 1000.0,
|
|
57
|
-
_tFl: 0
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
console.log(_ts);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
await pair();
|
package/src/types.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
declare module 'chacha' {
|
|
2
|
-
declare class Cipher {
|
|
3
|
-
setAAD(aad: Buffer): void;
|
|
4
|
-
getAuthTag(): Buffer;
|
|
5
|
-
setAuthTag(tag: Buffer): void;
|
|
6
|
-
_final(): void;
|
|
7
|
-
_update(chunk: Buffer): Buffer;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function createCipher(key: Buffer, nonce: Buffer): Cipher;
|
|
11
|
-
|
|
12
|
-
export function createDecipher(key: Buffer, nonce: Buffer): Cipher;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
declare module 'node-dns-sd' {
|
|
16
|
-
export function discover(opts: {
|
|
17
|
-
readonly name: string;
|
|
18
|
-
}): Promise<Result[]>;
|
|
19
|
-
|
|
20
|
-
export type Result = {
|
|
21
|
-
readonly fqdn: string;
|
|
22
|
-
readonly address: string;
|
|
23
|
-
readonly modelName: string;
|
|
24
|
-
readonly familyName: string | null;
|
|
25
|
-
readonly service: {
|
|
26
|
-
readonly port: number;
|
|
27
|
-
readonly protocol: 'tcp' | 'udp';
|
|
28
|
-
readonly type: string;
|
|
29
|
-
};
|
|
30
|
-
readonly packet: {
|
|
31
|
-
readonly address: string;
|
|
32
|
-
readonly header: Record<string, number>;
|
|
33
|
-
readonly questions: Array;
|
|
34
|
-
readonly answers: Array;
|
|
35
|
-
readonly authorities: Array;
|
|
36
|
-
readonly additionals: [];
|
|
37
|
-
};
|
|
38
|
-
readonly [key: string]: unknown;
|
|
39
|
-
};
|
|
40
|
-
}
|