appium-ios-remotexpc 0.21.1 → 0.22.0
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/CHANGELOG.md +13 -0
- package/build/src/lib/apple-tv/constants.d.ts +4 -3
- package/build/src/lib/apple-tv/constants.d.ts.map +1 -1
- package/build/src/lib/apple-tv/constants.js +10 -3
- package/build/src/lib/apple-tv/discovery/device-discovery.d.ts.map +1 -1
- package/build/src/lib/apple-tv/discovery/device-discovery.js +2 -2
- package/build/src/lib/apple-tv/encryption/index.d.ts +1 -0
- package/build/src/lib/apple-tv/encryption/index.d.ts.map +1 -1
- package/build/src/lib/apple-tv/encryption/index.js +1 -0
- package/build/src/lib/apple-tv/encryption/x25519.d.ts +8 -0
- package/build/src/lib/apple-tv/encryption/x25519.d.ts.map +1 -0
- package/build/src/lib/apple-tv/encryption/x25519.js +52 -0
- package/build/src/lib/apple-tv/index.d.ts +1 -0
- package/build/src/lib/apple-tv/index.d.ts.map +1 -1
- package/build/src/lib/apple-tv/index.js +1 -0
- package/build/src/lib/apple-tv/network/network-client.js +2 -2
- package/build/src/lib/apple-tv/pairing/user-input-service.d.ts.map +1 -1
- package/build/src/lib/apple-tv/pairing/user-input-service.js +2 -2
- package/build/src/lib/apple-tv/pairing-protocol/constants.d.ts +17 -0
- package/build/src/lib/apple-tv/pairing-protocol/constants.d.ts.map +1 -1
- package/build/src/lib/apple-tv/pairing-protocol/constants.js +25 -0
- package/build/src/lib/apple-tv/pairing-protocol/index.d.ts +2 -1
- package/build/src/lib/apple-tv/pairing-protocol/index.d.ts.map +1 -1
- package/build/src/lib/apple-tv/pairing-protocol/index.js +2 -1
- package/build/src/lib/apple-tv/pairing-protocol/pair-verification-protocol.d.ts +66 -0
- package/build/src/lib/apple-tv/pairing-protocol/pair-verification-protocol.d.ts.map +1 -0
- package/build/src/lib/apple-tv/pairing-protocol/pair-verification-protocol.js +178 -0
- package/build/src/lib/apple-tv/pairing-protocol/pairing-protocol.d.ts +35 -2
- package/build/src/lib/apple-tv/pairing-protocol/pairing-protocol.d.ts.map +1 -1
- package/build/src/lib/apple-tv/pairing-protocol/pairing-protocol.js +48 -14
- package/build/src/lib/apple-tv/storage/index.d.ts +1 -1
- package/build/src/lib/apple-tv/storage/index.d.ts.map +1 -1
- package/build/src/lib/apple-tv/storage/pairing-storage.d.ts +4 -1
- package/build/src/lib/apple-tv/storage/pairing-storage.d.ts.map +1 -1
- package/build/src/lib/apple-tv/storage/pairing-storage.js +59 -4
- package/build/src/lib/apple-tv/storage/types.d.ts +7 -1
- package/build/src/lib/apple-tv/storage/types.d.ts.map +1 -1
- package/build/src/lib/apple-tv/tunnel/index.d.ts +3 -0
- package/build/src/lib/apple-tv/tunnel/index.d.ts.map +1 -0
- package/build/src/lib/apple-tv/tunnel/index.js +1 -0
- package/build/src/lib/apple-tv/tunnel/tunnel-service.d.ts +57 -0
- package/build/src/lib/apple-tv/tunnel/tunnel-service.d.ts.map +1 -0
- package/build/src/lib/apple-tv/tunnel/tunnel-service.js +357 -0
- package/build/src/lib/apple-tv/tunnel/types.d.ts +22 -0
- package/build/src/lib/apple-tv/tunnel/types.d.ts.map +1 -0
- package/build/src/lib/apple-tv/tunnel/types.js +1 -0
- package/build/src/lib/bonjour/bonjour-discovery.d.ts.map +1 -1
- package/build/src/lib/bonjour/bonjour-discovery.js +3 -3
- package/build/src/lib/lockdown/index.d.ts.map +1 -1
- package/build/src/lib/plist/length-based-splitter.d.ts.map +1 -1
- package/build/src/lib/plist/length-based-splitter.js +0 -7
- package/build/src/lib/tunnel/index.d.ts +1 -0
- package/build/src/lib/tunnel/index.d.ts.map +1 -1
- package/build/src/lib/tunnel/packet-stream-server.d.ts.map +1 -1
- package/build/src/lib/tunnel/tunnel-registry-server.d.ts +1 -0
- package/build/src/lib/tunnel/tunnel-registry-server.d.ts.map +1 -1
- package/build/src/lib/tunnel/tunnel-registry-server.js +1 -1
- package/build/src/services/ios/afc/index.d.ts.map +1 -1
- package/build/src/services/ios/afc/index.js +14 -2
- package/build/src/services/ios/afc/stream-utils.d.ts.map +1 -1
- package/build/src/services/ios/afc/stream-utils.js +0 -2
- package/build/src/services/ios/mobile-config/index.js +2 -2
- package/package.json +2 -1
- package/scripts/pair-appletv.ts +2 -2
- package/scripts/start-appletv-tunnel.ts +178 -0
- package/scripts/test-tunnel-creation.ts +32 -23
- package/src/lib/apple-tv/constants.ts +11 -3
- package/src/lib/apple-tv/discovery/device-discovery.ts +2 -3
- package/src/lib/apple-tv/encryption/index.ts +6 -0
- package/src/lib/apple-tv/encryption/x25519.ts +79 -0
- package/src/lib/apple-tv/index.ts +1 -0
- package/src/lib/apple-tv/network/network-client.ts +2 -2
- package/src/lib/apple-tv/pairing/user-input-service.ts +2 -2
- package/src/lib/apple-tv/pairing-protocol/constants.ts +29 -0
- package/src/lib/apple-tv/pairing-protocol/index.ts +12 -1
- package/src/lib/apple-tv/pairing-protocol/pair-verification-protocol.ts +329 -0
- package/src/lib/apple-tv/pairing-protocol/pairing-protocol.ts +49 -19
- package/src/lib/apple-tv/storage/index.ts +1 -1
- package/src/lib/apple-tv/storage/pairing-storage.ts +73 -5
- package/src/lib/apple-tv/storage/types.ts +8 -1
- package/src/lib/apple-tv/tunnel/index.ts +2 -0
- package/src/lib/apple-tv/tunnel/tunnel-service.ts +543 -0
- package/src/lib/apple-tv/tunnel/types.ts +23 -0
- package/src/lib/bonjour/bonjour-discovery.ts +3 -5
- package/src/lib/lockdown/index.ts +0 -7
- package/src/lib/plist/length-based-splitter.ts +0 -22
- package/src/lib/tunnel/index.ts +2 -8
- package/src/lib/tunnel/packet-stream-server.ts +0 -8
- package/src/lib/tunnel/tunnel-registry-server.ts +1 -1
- package/src/services/ios/afc/index.ts +14 -3
- package/src/services/ios/afc/stream-utils.ts +0 -2
- package/src/services/ios/mobile-config/index.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## [0.22.0](https://github.com/appium/appium-ios-remotexpc/compare/v0.21.2...v0.22.0) (2026-01-02)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* **apple-tv:** add tunnel service for verified connections ([#109](https://github.com/appium/appium-ios-remotexpc/issues/109)) ([9e36c67](https://github.com/appium/appium-ios-remotexpc/commit/9e36c67396f84b795cea45a944df675465ec3139))
|
|
6
|
+
|
|
7
|
+
## [0.21.2](https://github.com/appium/appium-ios-remotexpc/compare/v0.21.1...v0.21.2) (2026-01-01)
|
|
8
|
+
|
|
9
|
+
### Miscellaneous Chores
|
|
10
|
+
|
|
11
|
+
* **deps:** bump actions/checkout from 6.0.0 to 6.0.1 ([#122](https://github.com/appium/appium-ios-remotexpc/issues/122)) ([05b9b9c](https://github.com/appium/appium-ios-remotexpc/commit/05b9b9c11d969b079104765006e692596813bee8))
|
|
12
|
+
* **deps:** bump actions/setup-node from 6.0.0 to 6.1.0 ([#123](https://github.com/appium/appium-ios-remotexpc/issues/123)) ([1d5695e](https://github.com/appium/appium-ios-remotexpc/commit/1d5695e0342e0110aaf21b2d8eecc8f38b3fcd77))
|
|
13
|
+
|
|
1
14
|
## [0.21.1](https://github.com/appium/appium-ios-remotexpc/compare/v0.21.0...v0.21.1) (2025-12-19)
|
|
2
15
|
|
|
3
16
|
### Miscellaneous Chores
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export declare const DEFAULT_PAIRING_CONFIG: {
|
|
2
|
-
readonly timeout:
|
|
3
|
-
readonly discoveryTimeout:
|
|
4
|
-
readonly maxRetries:
|
|
2
|
+
readonly timeout: number;
|
|
3
|
+
readonly discoveryTimeout: number;
|
|
4
|
+
readonly maxRetries: number;
|
|
5
5
|
};
|
|
6
|
+
export declare const APPLETV_PAIRING_PREFIX = "appletv_pairing_";
|
|
6
7
|
export declare const PairingDataComponentType: {
|
|
7
8
|
readonly METHOD: 0;
|
|
8
9
|
readonly IDENTIFIER: 1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/lib/apple-tv/constants.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/lib/apple-tv/constants.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,sBAAsB;;;;CAIzB,CAAC;AAGX,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AAGzD,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgC3B,CAAC;AAGX,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAG1C,eAAO,MAAM,cAAc,QAc1B,CAAC;AAGF,eAAO,MAAM,aAAa,QAAY,CAAC;AAGvC,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAG3C,eAAO,MAAM,YAAY,eAAe,CAAC;AAGzC,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAGxC,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAGxC,eAAO,MAAM,mBAAmB,WAAW,CAAC;AAG5C,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAGnC,eAAO,MAAM,WAAW,IAAO,CAAC;AAChC,eAAO,MAAM,WAAW,IAAO,CAAC;AAChC,eAAO,MAAM,YAAY,IAAO,CAAC;AACjC,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,eAAO,MAAM,oBAAoB,KAAO,CAAC;AACzC,eAAO,MAAM,uBAAuB,KAAO,CAAC;AAC5C,eAAO,MAAM,sBAAsB,KAAO,CAAC;AAC3C,eAAO,MAAM,sBAAsB,KAAK,CAAC;AACzC,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAGxC,eAAO,MAAM,kBAAkB,KAAO,CAAC;AACvC,eAAO,MAAM,mBAAmB,KAAO,CAAC;AACxC,eAAO,MAAM,mBAAmB,KAAO,CAAC;AACxC,eAAO,MAAM,mBAAmB,KAAO,CAAC;AAGxC,eAAO,MAAM,wBAAwB,KAAO,CAAC;AAC7C,eAAO,MAAM,6BAA6B,KAAO,CAAC;AAClD,eAAO,MAAM,8BAA8B,KAAO,CAAC;AACnD,eAAO,MAAM,8BAA8B,KAAO,CAAC;AAGnD,eAAO,MAAM,uBAAuB,MAAO,CAAC;AAC5C,eAAO,MAAM,4BAA4B,MAAO,CAAC;AACjD,eAAO,MAAM,6BAA6B,MAAO,CAAC;AAClD,eAAO,MAAM,6BAA6B,MAAO,CAAC;AAGlD,eAAO,MAAM,uBAAuB,MAAO,CAAC;AAC5C,eAAO,MAAM,4BAA4B,MAAO,CAAC;AAGjD,eAAO,MAAM,sBAAsB,MAAO,CAAC;AAC3C,eAAO,MAAM,2BAA2B,MAAO,CAAC;AAGhD,eAAO,MAAM,gBAAgB,MAAO,CAAC;AACrC,eAAO,MAAM,iBAAiB,QAAS,CAAC;AACxC,eAAO,MAAM,iBAAiB,aAAa,CAAC"}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
// Default configuration values for Apple TV pairing behavior
|
|
2
|
+
// These can be overridden via environment variables:
|
|
3
|
+
// - APPLETV_TIMEOUT: Network operation timeout in milliseconds (default: 30000)
|
|
4
|
+
// - APPLETV_DISCOVERY_TIMEOUT: Device discovery timeout in milliseconds (default: 10000)
|
|
5
|
+
// 10 seconds provides reliable Apple TV discovery via mDNS/Bonjour across various network conditions
|
|
6
|
+
// - APPLETV_MAX_RETRIES: Maximum retry attempts (default: 3)
|
|
2
7
|
export const DEFAULT_PAIRING_CONFIG = {
|
|
3
|
-
timeout: 30000,
|
|
4
|
-
discoveryTimeout:
|
|
5
|
-
maxRetries: 3,
|
|
8
|
+
timeout: Number(process.env.APPLETV_TIMEOUT) || 30000,
|
|
9
|
+
discoveryTimeout: Number(process.env.APPLETV_DISCOVERY_TIMEOUT) || 10000,
|
|
10
|
+
maxRetries: Number(process.env.APPLETV_MAX_RETRIES) || 3,
|
|
6
11
|
};
|
|
12
|
+
// Prefix used for Apple TV pairing record storage identifiers
|
|
13
|
+
export const APPLETV_PAIRING_PREFIX = 'appletv_pairing_';
|
|
7
14
|
// TLV8 component type identifiers used in pairing data exchange
|
|
8
15
|
export const PairingDataComponentType = {
|
|
9
16
|
METHOD: 0x00,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device-discovery.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/discovery/device-discovery.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"device-discovery.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/discovery/device-discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAEnB,MAAM,oCAAoC,CAAC;AAG5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,oEAAoE;AACpE,qBAAa,sBAAsB;IAGrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAuC;gBAE9B,MAAM,EAAE,aAAa;IAE5C,eAAe,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;CAkBlD"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { logger } from '@appium/support';
|
|
2
1
|
import { BonjourDiscovery, } from '../../bonjour/bonjour-discovery.js';
|
|
2
|
+
import { getLogger } from '../../logger.js';
|
|
3
3
|
import { PairingError } from '../errors.js';
|
|
4
4
|
/** Discovers Apple TV devices on the local network using Bonjour */
|
|
5
5
|
export class DeviceDiscoveryService {
|
|
6
6
|
config;
|
|
7
|
-
log =
|
|
7
|
+
log = getLogger('DeviceDiscoveryService');
|
|
8
8
|
constructor(config) {
|
|
9
9
|
this.config = config;
|
|
10
10
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { Opack2 } from './opack2.js';
|
|
2
2
|
export { encryptChaCha20Poly1305, decryptChaCha20Poly1305, type ChaCha20Poly1305Params, } from './chacha20-poly1305.js';
|
|
3
3
|
export { generateEd25519KeyPair, createEd25519Signature } from './ed25519.js';
|
|
4
|
+
export { generateX25519KeyPair, performX25519DiffieHellman, type X25519KeyPair, } from './x25519.js';
|
|
4
5
|
export { hkdf, type HKDFParams } from './hkdf.js';
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/encryption/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,KAAK,sBAAsB,GAC5B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE9E,OAAO,EAAE,IAAI,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/encryption/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,KAAK,sBAAsB,GAC5B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE9E,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,IAAI,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Opack2 } from './opack2.js';
|
|
2
2
|
export { encryptChaCha20Poly1305, decryptChaCha20Poly1305, } from './chacha20-poly1305.js';
|
|
3
3
|
export { generateEd25519KeyPair, createEd25519Signature } from './ed25519.js';
|
|
4
|
+
export { generateX25519KeyPair, performX25519DiffieHellman, } from './x25519.js';
|
|
4
5
|
export { hkdf } from './hkdf.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type KeyObject } from 'node:crypto';
|
|
2
|
+
export interface X25519KeyPair {
|
|
3
|
+
publicKey: Buffer;
|
|
4
|
+
privateKey: KeyObject;
|
|
5
|
+
}
|
|
6
|
+
export declare function generateX25519KeyPair(): X25519KeyPair;
|
|
7
|
+
export declare function performX25519DiffieHellman(privateKey: KeyObject, devicePublicKey: Buffer): Buffer;
|
|
8
|
+
//# sourceMappingURL=x25519.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"x25519.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/encryption/x25519.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EAIf,MAAM,aAAa,CAAC;AAYrB,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,SAAS,CAAC;CACvB;AAED,wBAAgB,qBAAqB,IAAI,aAAa,CAqBrD;AAED,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,SAAS,EACrB,eAAe,EAAE,MAAM,GACtB,MAAM,CA8BR"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createPublicKey, diffieHellman, generateKeyPairSync, } from 'node:crypto';
|
|
2
|
+
import { getLogger } from '../../logger.js';
|
|
3
|
+
import { CryptographyError } from '../errors.js';
|
|
4
|
+
const log = getLogger('X25519');
|
|
5
|
+
const X25519_PUBLIC_KEY_LENGTH = 32;
|
|
6
|
+
const X25519_SPKI_PREFIX = Buffer.from([
|
|
7
|
+
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00,
|
|
8
|
+
]);
|
|
9
|
+
export function generateX25519KeyPair() {
|
|
10
|
+
try {
|
|
11
|
+
const { publicKey, privateKey } = generateKeyPairSync('x25519');
|
|
12
|
+
const publicKeyDer = publicKey.export({
|
|
13
|
+
type: 'spki',
|
|
14
|
+
format: 'der',
|
|
15
|
+
});
|
|
16
|
+
const publicKeyBytes = publicKeyDer.subarray(-X25519_PUBLIC_KEY_LENGTH);
|
|
17
|
+
return {
|
|
18
|
+
publicKey: publicKeyBytes,
|
|
19
|
+
privateKey,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
log.error('Failed to generate X25519 key pair:', error);
|
|
24
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25
|
+
throw new CryptographyError(`Failed to generate X25519 key pair: ${message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function performX25519DiffieHellman(privateKey, devicePublicKey) {
|
|
29
|
+
if (!devicePublicKey || devicePublicKey.length !== X25519_PUBLIC_KEY_LENGTH) {
|
|
30
|
+
throw new CryptographyError(`Device public key must be ${X25519_PUBLIC_KEY_LENGTH} bytes`);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const devicePublicKeySpki = Buffer.concat([
|
|
34
|
+
X25519_SPKI_PREFIX,
|
|
35
|
+
devicePublicKey,
|
|
36
|
+
]);
|
|
37
|
+
const publicKeyObject = createPublicKey({
|
|
38
|
+
key: devicePublicKeySpki,
|
|
39
|
+
format: 'der',
|
|
40
|
+
type: 'spki',
|
|
41
|
+
});
|
|
42
|
+
return diffieHellman({
|
|
43
|
+
privateKey,
|
|
44
|
+
publicKey: publicKeyObject,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
log.error('Failed to perform X25519 Diffie-Hellman:', error);
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
throw new CryptographyError(`Failed to perform X25519 Diffie-Hellman: ${message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -9,6 +9,7 @@ export * from './srp/index.js';
|
|
|
9
9
|
export * from './network/index.js';
|
|
10
10
|
export * from './pairing-protocol/index.js';
|
|
11
11
|
export * from './storage/index.js';
|
|
12
|
+
export * from './tunnel/index.js';
|
|
12
13
|
export * from './discovery/index.js';
|
|
13
14
|
export * from './pairing/index.js';
|
|
14
15
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/apple-tv/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,aAAa,EACb,QAAQ,EACR,6BAA6B,EAC7B,WAAW,EACX,WAAW,EACX,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/apple-tv/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,aAAa,EACb,QAAQ,EACR,6BAA6B,EAC7B,WAAW,EACX,WAAW,EACX,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC"}
|
|
@@ -8,5 +8,6 @@ export * from './srp/index.js';
|
|
|
8
8
|
export * from './network/index.js';
|
|
9
9
|
export * from './pairing-protocol/index.js';
|
|
10
10
|
export * from './storage/index.js';
|
|
11
|
+
export * from './tunnel/index.js';
|
|
11
12
|
export * from './discovery/index.js';
|
|
12
13
|
export * from './pairing/index.js';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { logger } from '@appium/support';
|
|
2
1
|
import * as net from 'node:net';
|
|
2
|
+
import { getLogger } from '../../logger.js';
|
|
3
3
|
import { NetworkError } from '../errors.js';
|
|
4
4
|
import { NETWORK_CONSTANTS } from './constants.js';
|
|
5
|
-
const log =
|
|
5
|
+
const log = getLogger('NetworkClient');
|
|
6
6
|
/** Handles TCP socket communication with Apple TV devices */
|
|
7
7
|
export class NetworkClient {
|
|
8
8
|
config;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-input-service.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/pairing/user-input-service.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAEvE,4DAA4D;AAC5D,qBAAa,gBAAiB,YAAW,kBAAkB;IACzD,OAAO,CAAC,QAAQ,CAAC,GAAG,
|
|
1
|
+
{"version":3,"file":"user-input-service.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/pairing/user-input-service.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAEvE,4DAA4D;AAC5D,qBAAa,gBAAiB,YAAW,kBAAkB;IACzD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAiC;IAE/C,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAyC/B,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAkBtD"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { logger } from '@appium/support';
|
|
2
1
|
import { createInterface } from 'node:readline';
|
|
2
|
+
import { getLogger } from '../../logger.js';
|
|
3
3
|
import { PairingError } from '../errors.js';
|
|
4
4
|
import { NETWORK_CONSTANTS } from '../network/constants.js';
|
|
5
5
|
/** Handles user interaction for PIN input during pairing */
|
|
6
6
|
export class UserInputService {
|
|
7
|
-
log =
|
|
7
|
+
log = getLogger('UserInputService');
|
|
8
8
|
async promptForPIN() {
|
|
9
9
|
const rl = createInterface({
|
|
10
10
|
input: process.stdin,
|
|
@@ -13,6 +13,23 @@ export declare const PAIRING_MESSAGES: {
|
|
|
13
13
|
readonly M5_NONCE: "PS-Msg05";
|
|
14
14
|
readonly M6_NONCE: "PS-Msg06";
|
|
15
15
|
};
|
|
16
|
+
export declare const PAIR_VERIFY_MESSAGES: {
|
|
17
|
+
readonly ENCRYPT_SALT: "Pair-Verify-Encrypt-Salt";
|
|
18
|
+
readonly ENCRYPT_INFO: "Pair-Verify-Encrypt-Info";
|
|
19
|
+
readonly STATE_03_NONCE: "PV-Msg03";
|
|
20
|
+
};
|
|
21
|
+
export declare const PAIR_VERIFY_STATES: {
|
|
22
|
+
readonly STATE_01: 1;
|
|
23
|
+
readonly STATE_02: 2;
|
|
24
|
+
readonly STATE_03: 3;
|
|
25
|
+
readonly STATE_04: 4;
|
|
26
|
+
};
|
|
27
|
+
export declare const ENCRYPTION_MESSAGES: {
|
|
28
|
+
readonly CLIENT_ENCRYPT: "ClientEncrypt-main";
|
|
29
|
+
readonly SERVER_ENCRYPT: "ServerEncrypt-main";
|
|
30
|
+
};
|
|
31
|
+
/** Error descriptions for pair verification STATE=4 errors */
|
|
32
|
+
export declare const PAIR_VERIFY_ERROR_DESCRIPTIONS: Record<number, string>;
|
|
16
33
|
/** TLV type for device info */
|
|
17
34
|
export declare const INFO_TYPE = 17;
|
|
18
35
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/pairing-protocol/constants.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,eAAO,MAAM,cAAc;;;;CAIjB,CAAC;AAEX,8CAA8C;AAC9C,eAAO,MAAM,gBAAgB;;;;;;;CAOnB,CAAC;AAEX,+BAA+B;AAC/B,eAAO,MAAM,SAAS,KAAO,CAAC"}
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/pairing-protocol/constants.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,eAAO,MAAM,cAAc;;;;CAIjB,CAAC;AAEX,8CAA8C;AAC9C,eAAO,MAAM,gBAAgB;;;;;;;CAOnB,CAAC;AAEX,eAAO,MAAM,oBAAoB;;;;CAIvB,CAAC;AAEX,eAAO,MAAM,kBAAkB;;;;;CAKrB,CAAC;AAEX,eAAO,MAAM,mBAAmB;;;CAGtB,CAAC;AAEX,8DAA8D;AAC9D,eAAO,MAAM,8BAA8B,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAQxD,CAAC;AAEX,+BAA+B;AAC/B,eAAO,MAAM,SAAS,KAAO,CAAC"}
|
|
@@ -13,5 +13,30 @@ export const PAIRING_MESSAGES = {
|
|
|
13
13
|
M5_NONCE: 'PS-Msg05',
|
|
14
14
|
M6_NONCE: 'PS-Msg06',
|
|
15
15
|
};
|
|
16
|
+
export const PAIR_VERIFY_MESSAGES = {
|
|
17
|
+
ENCRYPT_SALT: 'Pair-Verify-Encrypt-Salt',
|
|
18
|
+
ENCRYPT_INFO: 'Pair-Verify-Encrypt-Info',
|
|
19
|
+
STATE_03_NONCE: 'PV-Msg03',
|
|
20
|
+
};
|
|
21
|
+
export const PAIR_VERIFY_STATES = {
|
|
22
|
+
STATE_01: 0x01,
|
|
23
|
+
STATE_02: 0x02,
|
|
24
|
+
STATE_03: 0x03,
|
|
25
|
+
STATE_04: 0x04,
|
|
26
|
+
};
|
|
27
|
+
export const ENCRYPTION_MESSAGES = {
|
|
28
|
+
CLIENT_ENCRYPT: 'ClientEncrypt-main',
|
|
29
|
+
SERVER_ENCRYPT: 'ServerEncrypt-main',
|
|
30
|
+
};
|
|
31
|
+
/** Error descriptions for pair verification STATE=4 errors */
|
|
32
|
+
export const PAIR_VERIFY_ERROR_DESCRIPTIONS = {
|
|
33
|
+
1: 'Unknown error',
|
|
34
|
+
2: 'Authentication failed - invalid pair record',
|
|
35
|
+
3: 'Backoff - too many attempts',
|
|
36
|
+
4: 'Max peers - device has too many connections',
|
|
37
|
+
5: 'Max tries exceeded',
|
|
38
|
+
6: 'Service unavailable',
|
|
39
|
+
7: 'Device busy',
|
|
40
|
+
};
|
|
16
41
|
/** TLV type for device info */
|
|
17
42
|
export const INFO_TYPE = 0x11;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { PairingProtocol } from './pairing-protocol.js';
|
|
2
|
-
export {
|
|
2
|
+
export { PairVerificationProtocol, type VerificationKeys, } from './pair-verification-protocol.js';
|
|
3
|
+
export { PAIRING_STATES, PAIRING_MESSAGES, INFO_TYPE, PAIR_VERIFY_MESSAGES, PAIR_VERIFY_STATES, ENCRYPTION_MESSAGES, } from './constants.js';
|
|
3
4
|
export type { EncryptionKeys, PairingRequest, UserInputInterface, PairingProtocolInterface, } from './types.js';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/pairing-protocol/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/pairing-protocol/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EACL,wBAAwB,EACxB,KAAK,gBAAgB,GACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,SAAS,EACT,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,wBAAwB,GACzB,MAAM,YAAY,CAAC"}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { PairingProtocol } from './pairing-protocol.js';
|
|
2
|
-
export {
|
|
2
|
+
export { PairVerificationProtocol, } from './pair-verification-protocol.js';
|
|
3
|
+
export { PAIRING_STATES, PAIRING_MESSAGES, INFO_TYPE, PAIR_VERIFY_MESSAGES, PAIR_VERIFY_STATES, ENCRYPTION_MESSAGES, } from './constants.js';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { NetworkClientInterface } from '../network/types.js';
|
|
2
|
+
import type { PairRecord } from '../storage/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Implements the HomeKit Accessory Protocol (HAP) Pair-Verify process for Apple TV.
|
|
5
|
+
*
|
|
6
|
+
* Protocol Overview:
|
|
7
|
+
* This class implements the HAP Pair-Verify protocol, which establishes an encrypted
|
|
8
|
+
* session between a previously paired controller (this client) and an Apple TV accessory.
|
|
9
|
+
* Unlike Pair-Setup, Pair-Verify uses existing long-term keys to authenticate both parties
|
|
10
|
+
* and derive session-specific encryption keys without requiring user interaction.
|
|
11
|
+
*
|
|
12
|
+
* State Machine Flow (4 states):
|
|
13
|
+
* - STATE=1: Client sends ephemeral X25519 public key to device
|
|
14
|
+
* - STATE=2: Device responds with its ephemeral X25519 public key and encrypted proof
|
|
15
|
+
* - STATE=3: Client sends encrypted signature proving identity using Ed25519 private key
|
|
16
|
+
* - STATE=4: Device confirms verification success or returns error
|
|
17
|
+
*
|
|
18
|
+
* After successful verification, both parties derive shared session keys for encrypting
|
|
19
|
+
* all subsequent communication during this session.
|
|
20
|
+
*
|
|
21
|
+
* Technical Details:
|
|
22
|
+
* - Uses X25519 Elliptic Curve Diffie-Hellman for ephemeral key exchange (RFC 7748)
|
|
23
|
+
* - Employs Ed25519 signatures for authentication using long-term keys (RFC 8032)
|
|
24
|
+
* - Uses ChaCha20-Poly1305 for authenticated encryption (RFC 8439)
|
|
25
|
+
* - Derives session keys using HKDF with protocol-specific salt/info (RFC 5869)
|
|
26
|
+
* - Encodes messages in TLV8 (Type-Length-Value) format
|
|
27
|
+
*
|
|
28
|
+
* Security Properties:
|
|
29
|
+
* - Perfect Forward Secrecy: Each session uses unique ephemeral keys
|
|
30
|
+
* - Mutual Authentication: Both client and device prove their identities
|
|
31
|
+
* - Replay Protection: Ephemeral keys prevent replay attacks
|
|
32
|
+
*
|
|
33
|
+
* References:
|
|
34
|
+
* - HAP Specification: https://developer.apple.com/homekit/ (Apple Developer)
|
|
35
|
+
* - HAP-NodeJS (community implementation): https://github.com/homebridge/HAP-NodeJS
|
|
36
|
+
* - X25519 ECDH: https://datatracker.ietf.org/doc/html/rfc7748
|
|
37
|
+
* - Ed25519 Signatures: https://datatracker.ietf.org/doc/html/rfc8032
|
|
38
|
+
* - ChaCha20-Poly1305: https://datatracker.ietf.org/doc/html/rfc8439
|
|
39
|
+
* - HKDF: https://datatracker.ietf.org/doc/html/rfc5869
|
|
40
|
+
*
|
|
41
|
+
* @see PairingProtocol for the initial pairing process that generates the long-term keys
|
|
42
|
+
* @see PairRecord for the stored credentials used in verification
|
|
43
|
+
*/
|
|
44
|
+
export interface VerificationKeys {
|
|
45
|
+
encryptionKey: Buffer;
|
|
46
|
+
clientEncryptionKey: Buffer;
|
|
47
|
+
serverEncryptionKey: Buffer;
|
|
48
|
+
}
|
|
49
|
+
export declare class PairVerificationProtocol {
|
|
50
|
+
private readonly networkClient;
|
|
51
|
+
private readonly hostIdentifier;
|
|
52
|
+
private sequenceNumber;
|
|
53
|
+
constructor(networkClient: NetworkClientInterface);
|
|
54
|
+
verify(pairRecord: PairRecord, deviceId: string): Promise<VerificationKeys>;
|
|
55
|
+
getSequenceNumber(): number;
|
|
56
|
+
setSequenceNumber(value: number): void;
|
|
57
|
+
private processState2Response;
|
|
58
|
+
private computeSharedSecret;
|
|
59
|
+
private derivePairVerifyKey;
|
|
60
|
+
private validateState4Response;
|
|
61
|
+
private createPairingPayload;
|
|
62
|
+
private sendState1;
|
|
63
|
+
private sendState3;
|
|
64
|
+
private deriveEncryptionKeys;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=pair-verification-protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pair-verification-protocol.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/pairing-protocol/pair-verification-protocol.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAatD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,qBAAa,wBAAwB;IAIvB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAH1C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,cAAc,CAAa;gBAEN,aAAa,EAAE,sBAAsB;IAI5D,MAAM,CACV,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,gBAAgB,CAAC;IAgC5B,iBAAiB,IAAI,MAAM;IAI3B,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;YAIxB,qBAAqB;IAuCnC,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,mBAAmB;YASb,sBAAsB;IA6BpC,OAAO,CAAC,oBAAoB;YA2Bd,UAAU;YAcV,UAAU;IAoDxB,OAAO,CAAC,oBAAoB;CAyB7B"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {} from 'node:crypto';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { getLogger } from '../../logger.js';
|
|
4
|
+
import { PairingDataComponentType } from '../constants.js';
|
|
5
|
+
import { createEd25519Signature, encryptChaCha20Poly1305, generateX25519KeyPair, hkdf, performX25519DiffieHellman, } from '../encryption/index.js';
|
|
6
|
+
import { PairingError } from '../errors.js';
|
|
7
|
+
import { decodeTLV8ToDict, encodeTLV8 } from '../tlv/index.js';
|
|
8
|
+
import { generateHostId } from '../utils/uuid-generator.js';
|
|
9
|
+
import { ENCRYPTION_MESSAGES, PAIR_VERIFY_ERROR_DESCRIPTIONS, PAIR_VERIFY_MESSAGES, PAIR_VERIFY_STATES, } from './constants.js';
|
|
10
|
+
const log = getLogger('PairVerificationProtocol');
|
|
11
|
+
export class PairVerificationProtocol {
|
|
12
|
+
networkClient;
|
|
13
|
+
hostIdentifier;
|
|
14
|
+
sequenceNumber = 0;
|
|
15
|
+
constructor(networkClient) {
|
|
16
|
+
this.networkClient = networkClient;
|
|
17
|
+
this.hostIdentifier = generateHostId(hostname());
|
|
18
|
+
}
|
|
19
|
+
async verify(pairRecord, deviceId) {
|
|
20
|
+
log.debug('Starting pair verification (4-step process)');
|
|
21
|
+
const { publicKey, privateKey } = generateX25519KeyPair();
|
|
22
|
+
log.debug(' - STATE=1: Send X25519 public key to device');
|
|
23
|
+
await this.sendState1(publicKey);
|
|
24
|
+
const devicePublicKey = await this.processState2Response();
|
|
25
|
+
const sharedSecret = this.computeSharedSecret(privateKey, devicePublicKey);
|
|
26
|
+
const pairVerifyKey = this.derivePairVerifyKey(sharedSecret);
|
|
27
|
+
log.debug(' - STATE=3: Send encrypted signature using Ed25519 private key from pair record');
|
|
28
|
+
log.debug(` Using pair record: ${deviceId}`);
|
|
29
|
+
await this.sendState3(pairRecord, publicKey, devicePublicKey, pairVerifyKey);
|
|
30
|
+
await this.validateState4Response();
|
|
31
|
+
log.debug(' - STATE=4: Receive verification success from device');
|
|
32
|
+
return this.deriveEncryptionKeys(sharedSecret);
|
|
33
|
+
}
|
|
34
|
+
getSequenceNumber() {
|
|
35
|
+
return this.sequenceNumber;
|
|
36
|
+
}
|
|
37
|
+
setSequenceNumber(value) {
|
|
38
|
+
this.sequenceNumber = value;
|
|
39
|
+
}
|
|
40
|
+
async processState2Response() {
|
|
41
|
+
const state2Response = await this.networkClient.receiveResponse();
|
|
42
|
+
const pairingData = state2Response.message?.plain?._0?.event?._0?.pairingData?._0?.data;
|
|
43
|
+
if (!pairingData) {
|
|
44
|
+
throw new PairingError('No pairing data in STATE=2 response', 'STATE_2_NO_DATA');
|
|
45
|
+
}
|
|
46
|
+
const tlvData = decodeTLV8ToDict(Buffer.from(pairingData, 'base64'));
|
|
47
|
+
if (tlvData[PairingDataComponentType.ERROR]) {
|
|
48
|
+
const errorCode = tlvData[PairingDataComponentType.ERROR];
|
|
49
|
+
const errorDecimal = errorCode[0];
|
|
50
|
+
log.error(`Device returned error in STATE=2: ${errorCode.toString('hex')} (decimal: ${errorDecimal})`);
|
|
51
|
+
throw new PairingError(`Authentication failed at STATE=2 (error: ${errorDecimal})`, 'STATE_2_ERROR');
|
|
52
|
+
}
|
|
53
|
+
const devicePublicKey = tlvData[PairingDataComponentType.PUBLIC_KEY];
|
|
54
|
+
if (!devicePublicKey) {
|
|
55
|
+
throw new PairingError('No device public key in STATE=2', 'STATE_2_NO_PUBLIC_KEY');
|
|
56
|
+
}
|
|
57
|
+
log.debug(' - STATE=2: Receive devices X25519 public key + encrypted data');
|
|
58
|
+
return devicePublicKey;
|
|
59
|
+
}
|
|
60
|
+
computeSharedSecret(privateKey, devicePublicKey) {
|
|
61
|
+
return performX25519DiffieHellman(privateKey, devicePublicKey);
|
|
62
|
+
}
|
|
63
|
+
derivePairVerifyKey(sharedSecret) {
|
|
64
|
+
return hkdf({
|
|
65
|
+
ikm: sharedSecret,
|
|
66
|
+
salt: Buffer.from(PAIR_VERIFY_MESSAGES.ENCRYPT_SALT),
|
|
67
|
+
info: Buffer.from(PAIR_VERIFY_MESSAGES.ENCRYPT_INFO),
|
|
68
|
+
length: 32,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async validateState4Response() {
|
|
72
|
+
const state4Response = await this.networkClient.receiveResponse();
|
|
73
|
+
const state4Data = state4Response.message?.plain?._0?.event?._0?.pairingData?._0?.data;
|
|
74
|
+
if (!state4Data) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const state4TLV = decodeTLV8ToDict(Buffer.from(state4Data, 'base64'));
|
|
78
|
+
if (state4TLV[PairingDataComponentType.ERROR]) {
|
|
79
|
+
const errorCode = state4TLV[PairingDataComponentType.ERROR];
|
|
80
|
+
const errorDecimal = errorCode[0];
|
|
81
|
+
const errorDescription = PAIR_VERIFY_ERROR_DESCRIPTIONS[errorDecimal] || 'Unknown error';
|
|
82
|
+
log.error(`Device returned error in STATE=4: ${errorCode.toString('hex')} (decimal: ${errorDecimal})`);
|
|
83
|
+
log.error(`Error description: ${errorDescription}`);
|
|
84
|
+
throw new PairingError(`Pair verification failed: ${errorDescription}`, 'STATE_4_ERROR');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
createPairingPayload(data, startNewSession) {
|
|
88
|
+
return {
|
|
89
|
+
message: {
|
|
90
|
+
plain: {
|
|
91
|
+
_0: {
|
|
92
|
+
event: {
|
|
93
|
+
_0: {
|
|
94
|
+
pairingData: {
|
|
95
|
+
_0: {
|
|
96
|
+
data,
|
|
97
|
+
kind: 'verifyManualPairing',
|
|
98
|
+
startNewSession,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
originatedBy: 'host',
|
|
107
|
+
sequenceNumber: this.sequenceNumber++,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async sendState1(x25519PublicKey) {
|
|
111
|
+
const tlvData = encodeTLV8([
|
|
112
|
+
{
|
|
113
|
+
type: PairingDataComponentType.STATE,
|
|
114
|
+
data: Buffer.from([PAIR_VERIFY_STATES.STATE_01]),
|
|
115
|
+
},
|
|
116
|
+
{ type: PairingDataComponentType.PUBLIC_KEY, data: x25519PublicKey },
|
|
117
|
+
]);
|
|
118
|
+
const payload = this.createPairingPayload(tlvData.toString('base64'), true);
|
|
119
|
+
await this.networkClient.sendPacket(payload);
|
|
120
|
+
}
|
|
121
|
+
async sendState3(pairRecord, x25519PublicKey, devicePublicKey, pairVerifyEncryptionKey) {
|
|
122
|
+
const signData = Buffer.concat([
|
|
123
|
+
x25519PublicKey,
|
|
124
|
+
Buffer.from(this.hostIdentifier, 'utf8'),
|
|
125
|
+
devicePublicKey,
|
|
126
|
+
]);
|
|
127
|
+
const signature = createEd25519Signature(signData, pairRecord.privateKey);
|
|
128
|
+
const responseTLV = encodeTLV8([
|
|
129
|
+
{
|
|
130
|
+
type: PairingDataComponentType.IDENTIFIER,
|
|
131
|
+
data: Buffer.from(this.hostIdentifier, 'utf8'),
|
|
132
|
+
},
|
|
133
|
+
{ type: PairingDataComponentType.SIGNATURE, data: signature },
|
|
134
|
+
]);
|
|
135
|
+
const nonce = Buffer.concat([
|
|
136
|
+
Buffer.alloc(4),
|
|
137
|
+
Buffer.from(PAIR_VERIFY_MESSAGES.STATE_03_NONCE),
|
|
138
|
+
]);
|
|
139
|
+
const encryptedResponse = encryptChaCha20Poly1305({
|
|
140
|
+
plaintext: responseTLV,
|
|
141
|
+
key: pairVerifyEncryptionKey,
|
|
142
|
+
nonce,
|
|
143
|
+
});
|
|
144
|
+
const finalTLV = encodeTLV8([
|
|
145
|
+
{
|
|
146
|
+
type: PairingDataComponentType.STATE,
|
|
147
|
+
data: Buffer.from([PAIR_VERIFY_STATES.STATE_03]),
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
type: PairingDataComponentType.ENCRYPTED_DATA,
|
|
151
|
+
data: encryptedResponse,
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
const payload = this.createPairingPayload(finalTLV.toString('base64'), false);
|
|
155
|
+
await this.networkClient.sendPacket(payload);
|
|
156
|
+
}
|
|
157
|
+
deriveEncryptionKeys(sharedSecret) {
|
|
158
|
+
log.debug('Deriving main encryption keys');
|
|
159
|
+
const clientEncryptionKey = hkdf({
|
|
160
|
+
ikm: sharedSecret,
|
|
161
|
+
salt: null,
|
|
162
|
+
info: Buffer.from(ENCRYPTION_MESSAGES.CLIENT_ENCRYPT),
|
|
163
|
+
length: 32,
|
|
164
|
+
});
|
|
165
|
+
const serverEncryptionKey = hkdf({
|
|
166
|
+
ikm: sharedSecret,
|
|
167
|
+
salt: null,
|
|
168
|
+
info: Buffer.from(ENCRYPTION_MESSAGES.SERVER_ENCRYPT),
|
|
169
|
+
length: 32,
|
|
170
|
+
});
|
|
171
|
+
log.debug('Derived client/server encryption keys using HKDF');
|
|
172
|
+
return {
|
|
173
|
+
encryptionKey: sharedSecret,
|
|
174
|
+
clientEncryptionKey,
|
|
175
|
+
serverEncryptionKey,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -1,11 +1,44 @@
|
|
|
1
1
|
import type { AppleTVDevice } from '../../bonjour/bonjour-discovery.js';
|
|
2
2
|
import type { NetworkClientInterface } from '../network/types.js';
|
|
3
3
|
import type { PairingProtocolInterface, UserInputInterface } from './types.js';
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Implements the HomeKit Accessory Protocol (HAP) Pair-Setup process for Apple TV.
|
|
6
|
+
*
|
|
7
|
+
* Protocol Overview:
|
|
8
|
+
* This class implements the HAP Pair-Setup protocol, which establishes a secure pairing
|
|
9
|
+
* between a controller (this client) and an Apple TV accessory. The protocol uses SRP
|
|
10
|
+
* (Secure Remote Password) authentication to verify a user-provided PIN without transmitting
|
|
11
|
+
* the PIN itself over the network.
|
|
12
|
+
*
|
|
13
|
+
* Message Exchange Flow (M1-M6):
|
|
14
|
+
* - M1/M2: Initial setup request and SRP challenge (salt + server public key)
|
|
15
|
+
* - M3/M4: Client sends SRP proof, server validates and responds
|
|
16
|
+
* - M5/M6: Exchange of long-term public keys and signatures (encrypted)
|
|
17
|
+
*
|
|
18
|
+
* After successful pairing, the generated Ed25519 key pair is stored and used for
|
|
19
|
+
* subsequent Pair-Verify operations to establish encrypted sessions.
|
|
20
|
+
*
|
|
21
|
+
* Technical Details:
|
|
22
|
+
* - Uses SRP-6a protocol for password-authenticated key exchange (RFC 5054)
|
|
23
|
+
* - Employs Ed25519 for long-term identity keys (RFC 8032)
|
|
24
|
+
* - Uses ChaCha20-Poly1305 for authenticated encryption (RFC 8439)
|
|
25
|
+
* - Derives session keys using HKDF (RFC 5869)
|
|
26
|
+
* - Encodes messages in TLV8 (Type-Length-Value) format
|
|
27
|
+
*
|
|
28
|
+
* References:
|
|
29
|
+
* - HAP Specification: https://developer.apple.com/homekit/ (Apple Developer)
|
|
30
|
+
* - HAP-NodeJS (community implementation): https://github.com/homebridge/HAP-NodeJS
|
|
31
|
+
* - SRP Protocol: https://datatracker.ietf.org/doc/html/rfc5054
|
|
32
|
+
* - Ed25519: https://datatracker.ietf.org/doc/html/rfc8032
|
|
33
|
+
* - ChaCha20-Poly1305: https://datatracker.ietf.org/doc/html/rfc8439
|
|
34
|
+
* - HKDF: https://datatracker.ietf.org/doc/html/rfc5869
|
|
35
|
+
*
|
|
36
|
+
* @see PairVerificationProtocol for the verification protocol used after pairing
|
|
37
|
+
* @see SRPClient for SRP authentication implementation
|
|
38
|
+
*/
|
|
5
39
|
export declare class PairingProtocol implements PairingProtocolInterface {
|
|
6
40
|
private readonly networkClient;
|
|
7
41
|
private readonly userInput;
|
|
8
|
-
private static readonly log;
|
|
9
42
|
private _sequenceNumber;
|
|
10
43
|
constructor(networkClient: NetworkClientInterface, userInput: UserInputInterface);
|
|
11
44
|
executePairingFlow(device: AppleTVDevice): Promise<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pairing-protocol.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/pairing-protocol/pairing-protocol.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pairing-protocol.d.ts","sourceRoot":"","sources":["../../../../../src/lib/apple-tv/pairing-protocol/pairing-protocol.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAgBxE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAYlE,OAAO,KAAK,EAEV,wBAAwB,EAExB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAIpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,qBAAa,eAAgB,YAAW,wBAAwB;IAI5D,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAJ5B,OAAO,CAAC,eAAe,CAAK;gBAGT,aAAa,EAAE,sBAAsB,EACrC,SAAS,EAAE,kBAAkB;IAG1C,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAwChE,OAAO,CAAC,aAAa;IAYrB;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IAetB;;;;OAIG;IACH,OAAO,CAAC,WAAW;IAInB;;;OAGG;YACW,gBAAgB;IAO9B;;;OAGG;YACW,uBAAuB;IAUrC;;;;OAIG;YACW,kBAAkB;IAQhC;;;;;OAKG;IACH,OAAO,CAAC,6BAA6B;IAYrC;;;;;;OAMG;YACW,wBAAwB;IActC;;;;OAIG;YACW,mBAAmB;IAcjC;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAuB9B;;;;OAIG;IACH,OAAO,CAAC,6BAA6B;IAmBrC;;;;OAIG;IACH,OAAO,CAAC,6BAA6B;IAUrC;;;;OAIG;IACH,OAAO,CAAC,+BAA+B;IAmBvC;;;;;;OAMG;IACH,OAAO,CAAC,iBAAiB;IAqBzB;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAyB3B;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAsBvB;;;;OAIG;YACW,YAAY;IAmC1B;;;;;OAKG;IACH,OAAO,CAAC,wBAAwB;IAiBhC;;;;;;;;;OASG;YACW,aAAa;IA2E3B;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IAiCzB;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IAe5B;;;;;;;OAOG;YACW,mBAAmB;CAQlC"}
|