@bharper/atv-js 0.2.3
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/LICENSE.md +21 -0
- package/README.md +80 -0
- package/dist/airplay/auth.d.ts +24 -0
- package/dist/airplay/auth.d.ts.map +1 -0
- package/dist/airplay/auth.js +195 -0
- package/dist/airplay/auth.js.map +1 -0
- package/dist/bplist.d.ts +19 -0
- package/dist/bplist.d.ts.map +1 -0
- package/dist/bplist.js +141 -0
- package/dist/bplist.js.map +1 -0
- package/dist/companion/auth.d.ts +34 -0
- package/dist/companion/auth.d.ts.map +1 -0
- package/dist/companion/auth.js +119 -0
- package/dist/companion/auth.js.map +1 -0
- package/dist/companion/connection.d.ts +50 -0
- package/dist/companion/connection.d.ts.map +1 -0
- package/dist/companion/connection.js +170 -0
- package/dist/companion/connection.js.map +1 -0
- package/dist/companion/keyboard.d.ts +39 -0
- package/dist/companion/keyboard.d.ts.map +1 -0
- package/dist/companion/keyboard.js +127 -0
- package/dist/companion/keyboard.js.map +1 -0
- package/dist/companion/pairing_keepalive.d.ts +4 -0
- package/dist/companion/pairing_keepalive.d.ts.map +1 -0
- package/dist/companion/pairing_keepalive.js +68 -0
- package/dist/companion/pairing_keepalive.js.map +1 -0
- package/dist/companion/protocol.d.ts +64 -0
- package/dist/companion/protocol.d.ts.map +1 -0
- package/dist/companion/protocol.js +246 -0
- package/dist/companion/protocol.js.map +1 -0
- package/dist/companion/remote.d.ts +75 -0
- package/dist/companion/remote.d.ts.map +1 -0
- package/dist/companion/remote.js +142 -0
- package/dist/companion/remote.js.map +1 -0
- package/dist/crypto/chacha20.d.ts +27 -0
- package/dist/crypto/chacha20.d.ts.map +1 -0
- package/dist/crypto/chacha20.js +88 -0
- package/dist/crypto/chacha20.js.map +1 -0
- package/dist/crypto/hkdf.d.ts +6 -0
- package/dist/crypto/hkdf.d.ts.map +1 -0
- package/dist/crypto/hkdf.js +45 -0
- package/dist/crypto/hkdf.js.map +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +191 -0
- package/dist/index.js.map +1 -0
- package/dist/mdns.d.ts +20 -0
- package/dist/mdns.d.ts.map +1 -0
- package/dist/mdns.js +165 -0
- package/dist/mdns.js.map +1 -0
- package/dist/opack.d.ts +21 -0
- package/dist/opack.d.ts.map +1 -0
- package/dist/opack.js +350 -0
- package/dist/opack.js.map +1 -0
- package/dist/pairing/credentials.d.ts +23 -0
- package/dist/pairing/credentials.d.ts.map +1 -0
- package/dist/pairing/credentials.js +31 -0
- package/dist/pairing/credentials.js.map +1 -0
- package/dist/pairing/srp.d.ts +54 -0
- package/dist/pairing/srp.d.ts.map +1 -0
- package/dist/pairing/srp.js +221 -0
- package/dist/pairing/srp.js.map +1 -0
- package/dist/pairing/tlv.d.ts +26 -0
- package/dist/pairing/tlv.d.ts.map +1 -0
- package/dist/pairing/tlv.js +68 -0
- package/dist/pairing/tlv.js.map +1 -0
- package/examples/pair.ts +103 -0
- package/examples/remote.ts +212 -0
- package/package.json +33 -0
- package/src/airplay/auth.ts +207 -0
- package/src/bplist.ts +136 -0
- package/src/companion/auth.ts +141 -0
- package/src/companion/connection.ts +161 -0
- package/src/companion/keyboard.ts +155 -0
- package/src/companion/pairing_keepalive.ts +75 -0
- package/src/companion/protocol.ts +253 -0
- package/src/companion/remote.ts +151 -0
- package/src/crypto/chacha20.ts +93 -0
- package/src/crypto/hkdf.ts +18 -0
- package/src/index.ts +248 -0
- package/src/mdns.ts +198 -0
- package/src/opack.ts +299 -0
- package/src/pairing/credentials.ts +44 -0
- package/src/pairing/srp.ts +234 -0
- package/src/pairing/tlv.ts +64 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SRP authentication handler for HAP pair-setup and pair-verify.
|
|
4
|
+
* Port of pyatv/auth/hap_srp.py SRPAuthHandler.
|
|
5
|
+
*
|
|
6
|
+
* Uses fast-srp-hap for SRP 3072-bit with SHA-512,
|
|
7
|
+
* and Node.js crypto for Ed25519 and X25519.
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.SRPAuthHandler = void 0;
|
|
44
|
+
const crypto = __importStar(require("crypto"));
|
|
45
|
+
const fast_srp_hap_1 = require("fast-srp-hap");
|
|
46
|
+
const chacha20_1 = require("../crypto/chacha20");
|
|
47
|
+
const hkdf_1 = require("../crypto/hkdf");
|
|
48
|
+
const tlv_1 = require("./tlv");
|
|
49
|
+
const opack_1 = require("../opack");
|
|
50
|
+
// Ed25519 DER prefixes for raw key import/export
|
|
51
|
+
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
52
|
+
const ED25519_PKCS8_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
|
|
53
|
+
const X25519_SPKI_PREFIX = Buffer.from('302a300506032b656e032100', 'hex');
|
|
54
|
+
const X25519_PKCS8_PREFIX = Buffer.from('302e020100300506032b656e04220420', 'hex');
|
|
55
|
+
class SRPAuthHandler {
|
|
56
|
+
pairingId;
|
|
57
|
+
signingKey = null;
|
|
58
|
+
authPrivate = null;
|
|
59
|
+
authPublic = null;
|
|
60
|
+
verifyPrivate = null;
|
|
61
|
+
verifyPublic = null;
|
|
62
|
+
srpClient = null;
|
|
63
|
+
shared = null;
|
|
64
|
+
sessionKey = null;
|
|
65
|
+
pin = '';
|
|
66
|
+
clientSecret = null;
|
|
67
|
+
constructor() {
|
|
68
|
+
this.pairingId = Buffer.from(crypto.randomUUID(), 'utf-8');
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Initialize by generating new Ed25519 signing keys and X25519 verify keys.
|
|
72
|
+
* Returns [authPublic, verifyPublic].
|
|
73
|
+
*/
|
|
74
|
+
initialize() {
|
|
75
|
+
// Generate raw 32-byte seeds first, like pyatv does
|
|
76
|
+
// pyatv: self._signing_key = Ed25519PrivateKey.from_private_bytes(os.urandom(32))
|
|
77
|
+
const authSeed = crypto.randomBytes(32);
|
|
78
|
+
const verifySeed = crypto.randomBytes(32);
|
|
79
|
+
// Create Ed25519 signing key from seed
|
|
80
|
+
// pyatv exports with Encoding.Raw, Format.Raw which gives the 32-byte seed
|
|
81
|
+
this.signingKey = crypto.createPrivateKey({
|
|
82
|
+
key: Buffer.concat([ED25519_PKCS8_PREFIX, authSeed]),
|
|
83
|
+
format: 'der',
|
|
84
|
+
type: 'pkcs8',
|
|
85
|
+
});
|
|
86
|
+
this.authPrivate = authSeed;
|
|
87
|
+
this.authPublic = crypto.createPublicKey(this.signingKey)
|
|
88
|
+
.export({ type: 'spki', format: 'der' }).subarray(-32);
|
|
89
|
+
// X25519 verify key
|
|
90
|
+
this.verifyPrivate = crypto.createPrivateKey({
|
|
91
|
+
key: Buffer.concat([X25519_PKCS8_PREFIX, verifySeed]),
|
|
92
|
+
format: 'der',
|
|
93
|
+
type: 'pkcs8',
|
|
94
|
+
});
|
|
95
|
+
this.verifyPublic = crypto.createPublicKey(this.verifyPrivate)
|
|
96
|
+
.export({ type: 'spki', format: 'der' }).subarray(-32);
|
|
97
|
+
// Use the raw seed as SRP client secret (matches pyatv behavior)
|
|
98
|
+
// pyatv uses binascii.hexlify(self._auth_private) as the SRP exponent 'a'
|
|
99
|
+
this.clientSecret = this.authPrivate;
|
|
100
|
+
return [this.authPublic, this.verifyPublic];
|
|
101
|
+
}
|
|
102
|
+
// ---- Pair-Verify ----
|
|
103
|
+
/**
|
|
104
|
+
* Pair-Verify step 1: X25519 shared secret + decrypt server identity + sign our identity.
|
|
105
|
+
*/
|
|
106
|
+
verify1(credentials, sessionPubKey, encrypted) {
|
|
107
|
+
const serverKey = crypto.createPublicKey({
|
|
108
|
+
key: Buffer.concat([X25519_SPKI_PREFIX, sessionPubKey]),
|
|
109
|
+
format: 'der',
|
|
110
|
+
type: 'spki',
|
|
111
|
+
});
|
|
112
|
+
this.shared = crypto.diffieHellman({
|
|
113
|
+
privateKey: this.verifyPrivate,
|
|
114
|
+
publicKey: serverKey,
|
|
115
|
+
});
|
|
116
|
+
const verifyKey = (0, hkdf_1.hkdfExpand)('Pair-Verify-Encrypt-Salt', 'Pair-Verify-Encrypt-Info', this.shared);
|
|
117
|
+
const chacha = new chacha20_1.Chacha20Cipher8byteNonce(verifyKey, verifyKey);
|
|
118
|
+
const decryptedBytes = chacha.decrypt(encrypted, Buffer.from('PV-Msg02', 'utf-8'));
|
|
119
|
+
const decryptedTlv = (0, tlv_1.readTlv)(decryptedBytes);
|
|
120
|
+
const identifier = decryptedTlv.get(tlv_1.TlvValue.Identifier);
|
|
121
|
+
const signature = decryptedTlv.get(tlv_1.TlvValue.Signature);
|
|
122
|
+
if (!identifier.equals(credentials.atvId)) {
|
|
123
|
+
throw new Error('Incorrect device response: identifier mismatch');
|
|
124
|
+
}
|
|
125
|
+
// Verify server Ed25519 signature
|
|
126
|
+
const info = Buffer.concat([sessionPubKey, identifier, this.verifyPublic]);
|
|
127
|
+
const ltpk = crypto.createPublicKey({
|
|
128
|
+
key: Buffer.concat([ED25519_SPKI_PREFIX, credentials.ltpk]),
|
|
129
|
+
format: 'der',
|
|
130
|
+
type: 'spki',
|
|
131
|
+
});
|
|
132
|
+
if (!crypto.verify(null, info, ltpk, signature)) {
|
|
133
|
+
throw new Error('Signature verification failed');
|
|
134
|
+
}
|
|
135
|
+
// Sign our identity
|
|
136
|
+
const deviceInfo = Buffer.concat([this.verifyPublic, credentials.clientId, sessionPubKey]);
|
|
137
|
+
const ltsk = crypto.createPrivateKey({
|
|
138
|
+
key: Buffer.concat([ED25519_PKCS8_PREFIX, credentials.ltsk]),
|
|
139
|
+
format: 'der',
|
|
140
|
+
type: 'pkcs8',
|
|
141
|
+
});
|
|
142
|
+
const deviceSignature = crypto.sign(null, deviceInfo, ltsk);
|
|
143
|
+
const tlv = (0, tlv_1.writeTlv)(new Map([
|
|
144
|
+
[tlv_1.TlvValue.Identifier, credentials.clientId],
|
|
145
|
+
[tlv_1.TlvValue.Signature, deviceSignature],
|
|
146
|
+
]));
|
|
147
|
+
return chacha.encrypt(tlv, Buffer.from('PV-Msg03', 'utf-8'));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Pair-Verify step 2: derive final encryption keys.
|
|
151
|
+
*/
|
|
152
|
+
verify2(salt, outputInfo, inputInfo) {
|
|
153
|
+
if (!this.shared)
|
|
154
|
+
throw new Error('Must call verify1 first');
|
|
155
|
+
const outputKey = (0, hkdf_1.hkdfExpand)(salt, outputInfo, this.shared);
|
|
156
|
+
const inputKey = (0, hkdf_1.hkdfExpand)(salt, inputInfo, this.shared);
|
|
157
|
+
return [outputKey, inputKey];
|
|
158
|
+
}
|
|
159
|
+
// ---- Pair-Setup ----
|
|
160
|
+
/**
|
|
161
|
+
* Pair-Setup step 1: store PIN for later use with salt.
|
|
162
|
+
*/
|
|
163
|
+
step1(pin) {
|
|
164
|
+
this.pin = pin;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Pair-Setup step 2: process server's public key and salt, compute SRP proof.
|
|
168
|
+
* Returns [clientPublicKey, clientProof].
|
|
169
|
+
*/
|
|
170
|
+
step2(atvPubKey, atvSalt) {
|
|
171
|
+
// HAP uses 3072-bit SRP with SHA-512 - use the 'hap' preset
|
|
172
|
+
const params = fast_srp_hap_1.SRP.params.hap;
|
|
173
|
+
this.srpClient = new fast_srp_hap_1.SrpClient(params, atvSalt, Buffer.from('Pair-Setup', 'utf-8'), Buffer.from(this.pin, 'utf-8'), this.clientSecret);
|
|
174
|
+
this.srpClient.setB(atvPubKey);
|
|
175
|
+
const pubKey = this.srpClient.computeA();
|
|
176
|
+
const proof = this.srpClient.computeM1();
|
|
177
|
+
return [pubKey, proof];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Pair-Setup step 3: sign identity and encrypt with session key.
|
|
181
|
+
* Returns encrypted data to send as SeqNo 0x05.
|
|
182
|
+
*/
|
|
183
|
+
step3(name) {
|
|
184
|
+
const srpKey = this.srpClient.computeK();
|
|
185
|
+
const iosDeviceX = (0, hkdf_1.hkdfExpand)('Pair-Setup-Controller-Sign-Salt', 'Pair-Setup-Controller-Sign-Info', srpKey);
|
|
186
|
+
this.sessionKey = (0, hkdf_1.hkdfExpand)('Pair-Setup-Encrypt-Salt', 'Pair-Setup-Encrypt-Info', srpKey);
|
|
187
|
+
const deviceInfo = Buffer.concat([iosDeviceX, this.pairingId, this.authPublic]);
|
|
188
|
+
const deviceSignature = crypto.sign(null, deviceInfo, this.signingKey);
|
|
189
|
+
const tlvData = new Map([
|
|
190
|
+
[tlv_1.TlvValue.Identifier, this.pairingId],
|
|
191
|
+
[tlv_1.TlvValue.PublicKey, this.authPublic],
|
|
192
|
+
[tlv_1.TlvValue.Signature, deviceSignature],
|
|
193
|
+
]);
|
|
194
|
+
if (name) {
|
|
195
|
+
tlvData.set(tlv_1.TlvValue.Name, (0, opack_1.pack)({ name }));
|
|
196
|
+
}
|
|
197
|
+
const chacha = new chacha20_1.Chacha20Cipher8byteNonce(this.sessionKey, this.sessionKey);
|
|
198
|
+
return chacha.encrypt((0, tlv_1.writeTlv)(tlvData), Buffer.from('PS-Msg05', 'utf-8'));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Pair-Setup step 4: decrypt device response and extract credentials.
|
|
202
|
+
*/
|
|
203
|
+
step4(encryptedData) {
|
|
204
|
+
const chacha = new chacha20_1.Chacha20Cipher8byteNonce(this.sessionKey, this.sessionKey);
|
|
205
|
+
const decrypted = chacha.decrypt(encryptedData, Buffer.from('PS-Msg06', 'utf-8'));
|
|
206
|
+
if (!decrypted || decrypted.length === 0) {
|
|
207
|
+
throw new Error('Failed to decrypt pairing response');
|
|
208
|
+
}
|
|
209
|
+
const tlv = (0, tlv_1.readTlv)(decrypted);
|
|
210
|
+
const atvIdentifier = tlv.get(tlv_1.TlvValue.Identifier);
|
|
211
|
+
const atvPubKey = tlv.get(tlv_1.TlvValue.PublicKey);
|
|
212
|
+
return {
|
|
213
|
+
ltpk: atvPubKey,
|
|
214
|
+
ltsk: this.authPrivate,
|
|
215
|
+
atvId: atvIdentifier,
|
|
216
|
+
clientId: this.pairingId,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
exports.SRPAuthHandler = SRPAuthHandler;
|
|
221
|
+
//# sourceMappingURL=srp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"srp.js","sourceRoot":"","sources":["../../src/pairing/srp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLV8 encoding/decoding for HAP (HomeKit Accessory Protocol).
|
|
3
|
+
* Direct port of pyatv/auth/hap_tlv8.py.
|
|
4
|
+
*/
|
|
5
|
+
export declare enum TlvValue {
|
|
6
|
+
Method = 0,
|
|
7
|
+
Identifier = 1,
|
|
8
|
+
Salt = 2,
|
|
9
|
+
PublicKey = 3,
|
|
10
|
+
Proof = 4,
|
|
11
|
+
EncryptedData = 5,
|
|
12
|
+
SeqNo = 6,
|
|
13
|
+
Error = 7,
|
|
14
|
+
BackOff = 8,
|
|
15
|
+
Certificate = 9,
|
|
16
|
+
Signature = 10,
|
|
17
|
+
Permissions = 11,
|
|
18
|
+
FragmentData = 12,
|
|
19
|
+
FragmentLast = 13,
|
|
20
|
+
Name = 17,
|
|
21
|
+
Flags = 19
|
|
22
|
+
}
|
|
23
|
+
export type TlvData = Map<number, Buffer>;
|
|
24
|
+
export declare function readTlv(data: Buffer): TlvData;
|
|
25
|
+
export declare function writeTlv(data: Map<number, Buffer> | Record<number, Buffer>): Buffer;
|
|
26
|
+
//# sourceMappingURL=tlv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tlv.d.ts","sourceRoot":"","sources":["../../src/pairing/tlv.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,oBAAY,QAAQ;IAClB,MAAM,IAAO;IACb,UAAU,IAAO;IACjB,IAAI,IAAO;IACX,SAAS,IAAO;IAChB,KAAK,IAAO;IACZ,aAAa,IAAO;IACpB,KAAK,IAAO;IACZ,KAAK,IAAO;IACZ,OAAO,IAAO;IACd,WAAW,IAAO;IAClB,SAAS,KAAO;IAChB,WAAW,KAAO;IAClB,YAAY,KAAO;IACnB,YAAY,KAAO;IACnB,IAAI,KAAO;IACX,KAAK,KAAO;CACb;AAED,MAAM,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE1C,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAe7C;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAoBnF"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TLV8 encoding/decoding for HAP (HomeKit Accessory Protocol).
|
|
4
|
+
* Direct port of pyatv/auth/hap_tlv8.py.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.TlvValue = void 0;
|
|
8
|
+
exports.readTlv = readTlv;
|
|
9
|
+
exports.writeTlv = writeTlv;
|
|
10
|
+
var TlvValue;
|
|
11
|
+
(function (TlvValue) {
|
|
12
|
+
TlvValue[TlvValue["Method"] = 0] = "Method";
|
|
13
|
+
TlvValue[TlvValue["Identifier"] = 1] = "Identifier";
|
|
14
|
+
TlvValue[TlvValue["Salt"] = 2] = "Salt";
|
|
15
|
+
TlvValue[TlvValue["PublicKey"] = 3] = "PublicKey";
|
|
16
|
+
TlvValue[TlvValue["Proof"] = 4] = "Proof";
|
|
17
|
+
TlvValue[TlvValue["EncryptedData"] = 5] = "EncryptedData";
|
|
18
|
+
TlvValue[TlvValue["SeqNo"] = 6] = "SeqNo";
|
|
19
|
+
TlvValue[TlvValue["Error"] = 7] = "Error";
|
|
20
|
+
TlvValue[TlvValue["BackOff"] = 8] = "BackOff";
|
|
21
|
+
TlvValue[TlvValue["Certificate"] = 9] = "Certificate";
|
|
22
|
+
TlvValue[TlvValue["Signature"] = 10] = "Signature";
|
|
23
|
+
TlvValue[TlvValue["Permissions"] = 11] = "Permissions";
|
|
24
|
+
TlvValue[TlvValue["FragmentData"] = 12] = "FragmentData";
|
|
25
|
+
TlvValue[TlvValue["FragmentLast"] = 13] = "FragmentLast";
|
|
26
|
+
TlvValue[TlvValue["Name"] = 17] = "Name";
|
|
27
|
+
TlvValue[TlvValue["Flags"] = 19] = "Flags";
|
|
28
|
+
})(TlvValue || (exports.TlvValue = TlvValue = {}));
|
|
29
|
+
function readTlv(data) {
|
|
30
|
+
const result = new Map();
|
|
31
|
+
let pos = 0;
|
|
32
|
+
while (pos < data.length) {
|
|
33
|
+
const tag = data[pos];
|
|
34
|
+
const length = data[pos + 1];
|
|
35
|
+
const value = data.subarray(pos + 2, pos + 2 + length);
|
|
36
|
+
if (result.has(tag)) {
|
|
37
|
+
result.set(tag, Buffer.concat([result.get(tag), value]));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
result.set(tag, Buffer.from(value));
|
|
41
|
+
}
|
|
42
|
+
pos += 2 + length;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
function writeTlv(data) {
|
|
47
|
+
const entries = data instanceof Map ? Array.from(data.entries()) : Object.entries(data).map(([k, v]) => [Number(k), v]);
|
|
48
|
+
const parts = [];
|
|
49
|
+
for (const [key, value] of entries) {
|
|
50
|
+
const tag = Buffer.from([key]);
|
|
51
|
+
let pos = 0;
|
|
52
|
+
let remaining = value.length;
|
|
53
|
+
while (pos < value.length || remaining === 0) {
|
|
54
|
+
const size = Math.min(remaining, 255);
|
|
55
|
+
parts.push(tag);
|
|
56
|
+
parts.push(Buffer.from([size]));
|
|
57
|
+
if (size > 0) {
|
|
58
|
+
parts.push(value.subarray(pos, pos + size));
|
|
59
|
+
}
|
|
60
|
+
pos += size;
|
|
61
|
+
remaining -= size;
|
|
62
|
+
if (remaining === 0 && pos >= value.length)
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return Buffer.concat(parts);
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=tlv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tlv.js","sourceRoot":"","sources":["../../src/pairing/tlv.ts"],"names":[],"mappings":"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"}
|
package/examples/pair.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env npx ts-node
|
|
2
|
+
/**
|
|
3
|
+
* Scan for Apple TVs, select one, pair AirPlay + Companion, save credentials.
|
|
4
|
+
*
|
|
5
|
+
* Usage: npx ts-node examples/pair.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as readline from 'readline';
|
|
11
|
+
import {
|
|
12
|
+
scan,
|
|
13
|
+
startAirPlayPairing,
|
|
14
|
+
finishAirPlayPairing,
|
|
15
|
+
startCompanionPairing,
|
|
16
|
+
finishCompanionPairing,
|
|
17
|
+
serializeCredentials,
|
|
18
|
+
AppleTVDevice,
|
|
19
|
+
} from '../src/index';
|
|
20
|
+
|
|
21
|
+
const CREDS_PATH = path.join(__dirname, 'device.json');
|
|
22
|
+
|
|
23
|
+
function ask(rl: readline.Interface, question: string): Promise<string> {
|
|
24
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function main() {
|
|
28
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// --- Scan ---
|
|
32
|
+
console.log('Scanning for Apple TVs (5 seconds)...');
|
|
33
|
+
const devices = await scan(5000);
|
|
34
|
+
|
|
35
|
+
if (devices.length === 0) {
|
|
36
|
+
console.log('No devices found.');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log('\nFound devices:');
|
|
41
|
+
devices.forEach((d, i) => {
|
|
42
|
+
console.log(` [${i}] ${d.name} (${d.address}) — model: ${d.model || 'unknown'}`);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// --- Select ---
|
|
46
|
+
let device: AppleTVDevice;
|
|
47
|
+
if (devices.length === 1) {
|
|
48
|
+
device = devices[0];
|
|
49
|
+
console.log(`\nAuto-selecting: ${device.name}`);
|
|
50
|
+
} else {
|
|
51
|
+
const idx = await ask(rl, `\nSelect device [0-${devices.length - 1}]: `);
|
|
52
|
+
device = devices[parseInt(idx, 10)];
|
|
53
|
+
if (!device) {
|
|
54
|
+
console.log('Invalid selection.');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- AirPlay pairing (phase 1) ---
|
|
60
|
+
console.log(`\nStarting AirPlay pairing with ${device.name}...`);
|
|
61
|
+
console.log('A PIN should appear on your Apple TV screen.');
|
|
62
|
+
const airplaySession = await startAirPlayPairing(device);
|
|
63
|
+
|
|
64
|
+
const pin1 = await ask(rl, 'Enter the PIN shown on screen: ');
|
|
65
|
+
console.log('Completing AirPlay pairing...');
|
|
66
|
+
const airplayCreds = await finishAirPlayPairing(airplaySession, pin1.trim(), 'atv-js');
|
|
67
|
+
console.log('AirPlay pairing successful!');
|
|
68
|
+
|
|
69
|
+
// --- Companion pairing (phase 2) ---
|
|
70
|
+
console.log(`\nStarting Companion pairing with ${device.name}...`);
|
|
71
|
+
console.log('A new PIN should appear on your Apple TV screen.');
|
|
72
|
+
const companionSession = await startCompanionPairing(device);
|
|
73
|
+
|
|
74
|
+
const pin2 = await ask(rl, 'Enter the PIN shown on screen: ');
|
|
75
|
+
console.log('Completing Companion pairing...');
|
|
76
|
+
const companionCreds = await finishCompanionPairing(companionSession, pin2.trim(), 'atv-js');
|
|
77
|
+
console.log('Companion pairing successful!');
|
|
78
|
+
|
|
79
|
+
// --- Save credentials ---
|
|
80
|
+
const savedData = {
|
|
81
|
+
name: device.name,
|
|
82
|
+
address: device.address,
|
|
83
|
+
port: device.port,
|
|
84
|
+
airplayPort: device.airplayPort,
|
|
85
|
+
identifier: device.identifier,
|
|
86
|
+
model: device.model,
|
|
87
|
+
credentials: {
|
|
88
|
+
airplay: serializeCredentials(airplayCreds),
|
|
89
|
+
companion: serializeCredentials(companionCreds),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
fs.writeFileSync(CREDS_PATH, JSON.stringify(savedData, null, 2));
|
|
94
|
+
console.log(`\nCredentials saved to ${CREDS_PATH}`);
|
|
95
|
+
} catch (err: any) {
|
|
96
|
+
console.error('Error:', err.message || err);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
} finally {
|
|
99
|
+
rl.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main();
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env npx ts-node
|
|
2
|
+
/**
|
|
3
|
+
* Connect to a paired Apple TV, interactive remote control with keyboard input support.
|
|
4
|
+
*
|
|
5
|
+
* Usage: npx ts-node examples/remote.ts
|
|
6
|
+
*
|
|
7
|
+
* Reads credentials from examples/device.json (created by pair.ts).
|
|
8
|
+
* Uses raw terminal mode to capture individual keypresses.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as readline from 'readline';
|
|
14
|
+
import {
|
|
15
|
+
connect,
|
|
16
|
+
sendKey,
|
|
17
|
+
getText,
|
|
18
|
+
setText,
|
|
19
|
+
getKeyboardFocusState,
|
|
20
|
+
onConnectionLost,
|
|
21
|
+
disconnect,
|
|
22
|
+
isConnected,
|
|
23
|
+
RemoteKey,
|
|
24
|
+
AppleTVConnection,
|
|
25
|
+
} from '../src/index';
|
|
26
|
+
import { watchKeyboardFocus, KeyboardFocusState } from '../src/companion/keyboard';
|
|
27
|
+
|
|
28
|
+
const CREDS_PATH = path.join(__dirname, 'device.json');
|
|
29
|
+
|
|
30
|
+
// Keyboard mapping: terminal key → RemoteKey
|
|
31
|
+
const KEYMAP: Record<string, string> = {
|
|
32
|
+
'\x1b[D': RemoteKey.Left, // ArrowLeft
|
|
33
|
+
'\x1b[C': RemoteKey.Right, // ArrowRight
|
|
34
|
+
'\x1b[A': RemoteKey.Up, // ArrowUp
|
|
35
|
+
'\x1b[B': RemoteKey.Down, // ArrowDown
|
|
36
|
+
'\r': RemoteKey.Select, // Enter
|
|
37
|
+
' ': RemoteKey.PlayPause, // Space
|
|
38
|
+
'\x7f': RemoteKey.Menu, // Backspace
|
|
39
|
+
'\x1b': RemoteKey.Menu, // Escape (raw)
|
|
40
|
+
'n': RemoteKey.Next,
|
|
41
|
+
'p': RemoteKey.Previous,
|
|
42
|
+
']': RemoteKey.Next,
|
|
43
|
+
'[': RemoteKey.Previous,
|
|
44
|
+
't': RemoteKey.Home,
|
|
45
|
+
'l': RemoteKey.HomeHold,
|
|
46
|
+
'-': RemoteKey.VolumeDown,
|
|
47
|
+
'=': RemoteKey.VolumeUp
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Module-level cleanup function for keyboard watcher
|
|
51
|
+
let stopKeyboardWatcher: (() => void) | null = null;
|
|
52
|
+
|
|
53
|
+
function ask(rl: readline.Interface, question: string): Promise<string> {
|
|
54
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function main() {
|
|
58
|
+
// --- Load credentials ---
|
|
59
|
+
if (!fs.existsSync(CREDS_PATH)) {
|
|
60
|
+
console.error(`No credentials found at ${CREDS_PATH}`);
|
|
61
|
+
console.error('Run pair.ts first to pair with an Apple TV.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const saved = JSON.parse(fs.readFileSync(CREDS_PATH, 'utf-8'));
|
|
66
|
+
const device = {
|
|
67
|
+
name: saved.name,
|
|
68
|
+
address: saved.address,
|
|
69
|
+
port: saved.port,
|
|
70
|
+
airplayPort: saved.airplayPort,
|
|
71
|
+
identifier: saved.identifier,
|
|
72
|
+
model: saved.model,
|
|
73
|
+
properties: {},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
console.log(`Connecting to ${device.name} (${device.address})...`);
|
|
77
|
+
|
|
78
|
+
let conn: AppleTVConnection;
|
|
79
|
+
try {
|
|
80
|
+
conn = await connect(device, saved.credentials);
|
|
81
|
+
} catch (err: any) {
|
|
82
|
+
console.error('Connection failed:', err.message || err);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log('Connected!\n');
|
|
87
|
+
|
|
88
|
+
onConnectionLost(conn, (err) => {
|
|
89
|
+
console.log(`\nConnection lost${err ? ': ' + err.message : ''}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// --- Watch keyboard focus (polls every 1 second) ---
|
|
94
|
+
let keyboardFocused = false;
|
|
95
|
+
|
|
96
|
+
stopKeyboardWatcher = watchKeyboardFocus(conn.protocol, (state) => {
|
|
97
|
+
const wasFocused = keyboardFocused;
|
|
98
|
+
keyboardFocused = state === KeyboardFocusState.Focused;
|
|
99
|
+
|
|
100
|
+
if (keyboardFocused && !wasFocused) {
|
|
101
|
+
console.log('\n[Keyboard focused — entering text input mode]');
|
|
102
|
+
console.log('Type text and press Enter to send, or press Escape to go back.\n');
|
|
103
|
+
enterTextMode(conn);
|
|
104
|
+
} else if (!keyboardFocused && wasFocused) {
|
|
105
|
+
console.log('\n[Keyboard unfocused — back to remote mode]');
|
|
106
|
+
enterRemoteMode(conn);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// --- Start in remote mode ---
|
|
111
|
+
printRemoteHelp();
|
|
112
|
+
enterRemoteMode(conn);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function printRemoteHelp() {
|
|
116
|
+
console.log('--- Apple TV Remote ---');
|
|
117
|
+
console.log('Arrow keys : Navigate');
|
|
118
|
+
console.log('Enter : Select');
|
|
119
|
+
console.log('Space : Play/Pause');
|
|
120
|
+
console.log('Backspace : Menu/Back');
|
|
121
|
+
console.log('t : Home (TV button)');
|
|
122
|
+
console.log('l : Long-press Home');
|
|
123
|
+
console.log('n / ] : Next track');
|
|
124
|
+
console.log('p / [ : Previous track');
|
|
125
|
+
console.log('- / = : Volume down/up');
|
|
126
|
+
console.log('Ctrl+C : Quit');
|
|
127
|
+
console.log('');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function enterRemoteMode(conn: AppleTVConnection) {
|
|
131
|
+
if (!process.stdin.isTTY) {
|
|
132
|
+
console.error('stdin is not a TTY — raw mode not available');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
process.stdin.setRawMode(true);
|
|
137
|
+
process.stdin.resume();
|
|
138
|
+
process.stdin.removeAllListeners('data');
|
|
139
|
+
process.stdin.removeAllListeners('line');
|
|
140
|
+
|
|
141
|
+
process.stdin.on('data', async (data: Buffer) => {
|
|
142
|
+
const key = data.toString();
|
|
143
|
+
|
|
144
|
+
// Ctrl+C
|
|
145
|
+
if (key === '\x03') {
|
|
146
|
+
console.log('\nDisconnecting...');
|
|
147
|
+
if (stopKeyboardWatcher) stopKeyboardWatcher();
|
|
148
|
+
disconnect(conn);
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const mapped = KEYMAP[key];
|
|
153
|
+
if (mapped) {
|
|
154
|
+
try {
|
|
155
|
+
await sendKey(conn, mapped);
|
|
156
|
+
} catch (err: any) {
|
|
157
|
+
console.error(`Key error: ${err.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function enterTextMode(conn: AppleTVConnection) {
|
|
164
|
+
if (!process.stdin.isTTY) return;
|
|
165
|
+
|
|
166
|
+
// Switch out of raw mode so readline works normally
|
|
167
|
+
process.stdin.setRawMode(false);
|
|
168
|
+
process.stdin.removeAllListeners('data');
|
|
169
|
+
|
|
170
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
171
|
+
|
|
172
|
+
const promptForText = async () => {
|
|
173
|
+
// Show current text
|
|
174
|
+
try {
|
|
175
|
+
const current = await getText(conn);
|
|
176
|
+
if (current !== null) {
|
|
177
|
+
console.log(`Current text: "${current}"`);
|
|
178
|
+
}
|
|
179
|
+
} catch {}
|
|
180
|
+
|
|
181
|
+
rl.question('> ', async (input) => {
|
|
182
|
+
if (input === '\x1b' || input === '\\escape') {
|
|
183
|
+
// User wants to exit text mode — send Menu to dismiss keyboard
|
|
184
|
+
try {
|
|
185
|
+
await sendKey(conn, RemoteKey.Menu);
|
|
186
|
+
} catch {}
|
|
187
|
+
rl.close();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Set the text on the Apple TV
|
|
192
|
+
try {
|
|
193
|
+
await setText(conn, input);
|
|
194
|
+
console.log(`Text set to: "${input}"`);
|
|
195
|
+
} catch (err: any) {
|
|
196
|
+
console.error(`setText error: ${err.message}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Prompt again
|
|
200
|
+
promptForText();
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
rl.on('close', () => {
|
|
205
|
+
// Re-enter remote mode when readline closes
|
|
206
|
+
enterRemoteMode(conn);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
promptForText();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
main();
|