appium-ios-remotexpc 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +38 -0
- package/.github/workflows/format-check.yml +43 -0
- package/.github/workflows/lint-and-build.yml +40 -0
- package/.github/workflows/pr-title.yml +16 -0
- package/.github/workflows/publish.js.yml +42 -0
- package/.github/workflows/test-validation.yml +40 -0
- package/.mocharc.json +8 -0
- package/.prettierignore +3 -0
- package/.prettierrc +17 -0
- package/.releaserc +37 -0
- package/CHANGELOG.md +63 -0
- package/LICENSE +201 -0
- package/README.md +178 -0
- package/assets/images/ios-arch.png +0 -0
- package/eslint.config.js +45 -0
- package/package.json +78 -0
- package/scripts/test-tunnel-creation.ts +378 -0
- package/src/base-plist-service.ts +83 -0
- package/src/base-socket-service.ts +55 -0
- package/src/index.ts +34 -0
- package/src/lib/apple-tv/constants.ts +83 -0
- package/src/lib/apple-tv/errors.ts +31 -0
- package/src/lib/apple-tv/tlv/decoder.ts +68 -0
- package/src/lib/apple-tv/tlv/encoder.ts +33 -0
- package/src/lib/apple-tv/tlv/index.ts +6 -0
- package/src/lib/apple-tv/tlv/pairing-tlv.ts +31 -0
- package/src/lib/apple-tv/types.ts +58 -0
- package/src/lib/apple-tv/utils/buffer-utils.ts +90 -0
- package/src/lib/apple-tv/utils/index.ts +2 -0
- package/src/lib/apple-tv/utils/uuid-generator.ts +43 -0
- package/src/lib/lockdown/index.ts +468 -0
- package/src/lib/pair-record/index.ts +8 -0
- package/src/lib/pair-record/pair-record.ts +133 -0
- package/src/lib/plist/binary-plist-creator.ts +571 -0
- package/src/lib/plist/binary-plist-parser.ts +587 -0
- package/src/lib/plist/constants.ts +53 -0
- package/src/lib/plist/index.ts +54 -0
- package/src/lib/plist/length-based-splitter.ts +326 -0
- package/src/lib/plist/plist-creator.ts +42 -0
- package/src/lib/plist/plist-decoder.ts +135 -0
- package/src/lib/plist/plist-encoder.ts +36 -0
- package/src/lib/plist/plist-parser.ts +144 -0
- package/src/lib/plist/plist-service.ts +231 -0
- package/src/lib/plist/unified-plist-creator.ts +19 -0
- package/src/lib/plist/unified-plist-parser.ts +25 -0
- package/src/lib/plist/utils.ts +376 -0
- package/src/lib/remote-xpc/constants.ts +22 -0
- package/src/lib/remote-xpc/handshake-frames.ts +377 -0
- package/src/lib/remote-xpc/handshake.ts +152 -0
- package/src/lib/remote-xpc/remote-xpc-connection.ts +461 -0
- package/src/lib/remote-xpc/xpc-protocol.ts +412 -0
- package/src/lib/tunnel/index.ts +253 -0
- package/src/lib/tunnel/packet-stream-client.ts +185 -0
- package/src/lib/tunnel/packet-stream-server.ts +133 -0
- package/src/lib/tunnel/tunnel-api-client.ts +234 -0
- package/src/lib/tunnel/tunnel-registry-server.ts +410 -0
- package/src/lib/types.ts +291 -0
- package/src/lib/usbmux/index.ts +630 -0
- package/src/lib/usbmux/usbmux-decoder.ts +66 -0
- package/src/lib/usbmux/usbmux-encoder.ts +55 -0
- package/src/service-connection.ts +79 -0
- package/src/services/index.ts +15 -0
- package/src/services/ios/base-service.ts +81 -0
- package/src/services/ios/diagnostic-service/index.ts +241 -0
- package/src/services/ios/diagnostic-service/keys.ts +770 -0
- package/src/services/ios/syslog-service/index.ts +387 -0
- package/src/services/ios/tunnel-service/index.ts +88 -0
- package/src/services.ts +81 -0
- package/test/integration/diagnostics-test.ts +44 -0
- package/test/integration/read-pair-record-test.ts +39 -0
- package/test/integration/tunnel-test.ts +104 -0
- package/test/unit/apple-tv/tlv/decoder.spec.ts +144 -0
- package/test/unit/apple-tv/tlv/encoder.spec.ts +91 -0
- package/test/unit/apple-tv/tlv/pairing-tlv.spec.ts +101 -0
- package/test/unit/apple-tv/tlv/tlv-integration.spec.ts +146 -0
- package/test/unit/apple-tv/utils/buffer-utils.spec.ts +74 -0
- package/test/unit/apple-tv/utils/uuid-generator.spec.ts +39 -0
- package/test/unit/fixtures/index.ts +88 -0
- package/test/unit/fixtures/usbmuxconnectmessage.bin +0 -0
- package/test/unit/fixtures/usbmuxlistdevicemessage.bin +0 -0
- package/test/unit/plist/error-handling.spec.ts +101 -0
- package/test/unit/plist/fixtures/sample.binary.plist +0 -0
- package/test/unit/plist/fixtures/sample.xml.plist +38 -0
- package/test/unit/plist/plist-parser.spec.ts +283 -0
- package/test/unit/plist/plist.spec.ts +205 -0
- package/test/unit/plist/tag-position-handling.spec.ts +90 -0
- package/test/unit/plist/unified-plist-parser.spec.ts +227 -0
- package/test/unit/plist/utils.spec.ts +249 -0
- package/test/unit/plist/xml-cleaning.spec.ts +60 -0
- package/test/unit/tunnel/tunnel-registry-server.spec.ts +194 -0
- package/test/unit/usbmux/usbmux-specs.ts +71 -0
- package/tsconfig.json +36 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Socket } from 'node:net';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PlistService,
|
|
5
|
+
type PlistServiceOptions,
|
|
6
|
+
} from './lib/plist/plist-service.js';
|
|
7
|
+
import type { PlistDictionary } from './lib/types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Message type for plist communications
|
|
11
|
+
*/
|
|
12
|
+
type PlistMessage = PlistDictionary;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Base class for services that use PlistService for communication
|
|
16
|
+
*/
|
|
17
|
+
export abstract class BasePlistService {
|
|
18
|
+
/**
|
|
19
|
+
* Sends a message and waits for a response
|
|
20
|
+
* @param message The message to send
|
|
21
|
+
* @param timeout Timeout in milliseconds
|
|
22
|
+
* @returns Promise resolving to the response
|
|
23
|
+
*/
|
|
24
|
+
async sendAndReceive(
|
|
25
|
+
message: PlistMessage,
|
|
26
|
+
timeout?: number,
|
|
27
|
+
): Promise<PlistMessage> {
|
|
28
|
+
return this._plistService.sendPlistAndReceive(message, timeout);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Closes the underlying connection
|
|
33
|
+
*/
|
|
34
|
+
public close(): void {
|
|
35
|
+
this._plistService.close();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets the PlistService instance
|
|
40
|
+
* @returns The PlistService instance
|
|
41
|
+
*/
|
|
42
|
+
protected getPlistService(): PlistService {
|
|
43
|
+
return this._plistService;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The underlying PlistService instance
|
|
48
|
+
*/
|
|
49
|
+
protected _plistService: PlistService;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a new BasePlistService
|
|
53
|
+
* @param plistServiceOrSocket PlistService instance or Socket
|
|
54
|
+
* @param options Configuration options for PlistService
|
|
55
|
+
*/
|
|
56
|
+
protected constructor(
|
|
57
|
+
plistServiceOrSocket: PlistService | Socket,
|
|
58
|
+
options: PlistServiceOptions = {},
|
|
59
|
+
) {
|
|
60
|
+
if (plistServiceOrSocket instanceof PlistService) {
|
|
61
|
+
this._plistService = plistServiceOrSocket;
|
|
62
|
+
} else {
|
|
63
|
+
this._plistService = new PlistService(plistServiceOrSocket, options);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Sends a message without waiting for a response
|
|
69
|
+
* @param message The message to send
|
|
70
|
+
*/
|
|
71
|
+
protected send(message: PlistMessage): void {
|
|
72
|
+
this._plistService.sendPlist(message);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Waits for a message
|
|
77
|
+
* @param timeout Timeout in milliseconds
|
|
78
|
+
* @returns Promise resolving to the received message
|
|
79
|
+
*/
|
|
80
|
+
protected async receive(timeout?: number): Promise<PlistMessage> {
|
|
81
|
+
return this._plistService.receivePlist(timeout);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { Socket } from 'node:net';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
|
|
5
|
+
class BaseSocketService extends EventEmitter {
|
|
6
|
+
protected _socketClient: Socket;
|
|
7
|
+
protected _isConnected: boolean = false;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param socketClient
|
|
11
|
+
*/
|
|
12
|
+
constructor(socketClient: Socket) {
|
|
13
|
+
super();
|
|
14
|
+
this._socketClient = socketClient;
|
|
15
|
+
|
|
16
|
+
// Check if already connected
|
|
17
|
+
this._isConnected = !socketClient.connecting && !socketClient.destroyed;
|
|
18
|
+
|
|
19
|
+
// if not connected and it's a raw socket
|
|
20
|
+
if (!this._isConnected && socketClient instanceof Socket) {
|
|
21
|
+
this._socketClient.once('connect', () => {
|
|
22
|
+
this._isConnected = true;
|
|
23
|
+
this.emit('connect');
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// setup basic error handling
|
|
28
|
+
this._socketClient.on('error', (err) => {
|
|
29
|
+
this.emit('error', err);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this._socketClient.on('close', () => {
|
|
33
|
+
this._isConnected = false;
|
|
34
|
+
this.emit('close');
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_assignClientFailureHandlers(...sourceStreams: Readable[]): void {
|
|
39
|
+
for (const evt of ['close', 'end']) {
|
|
40
|
+
this._socketClient.once(evt, () => {
|
|
41
|
+
sourceStreams.map((s) => {
|
|
42
|
+
s.unpipe(this._socketClient);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
close(): void {
|
|
49
|
+
if (!this._socketClient.destroyed) {
|
|
50
|
+
this._socketClient.end();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { BaseSocketService };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createLockdownServiceByUDID } from './lib/lockdown/index.js';
|
|
2
|
+
import {
|
|
3
|
+
PacketStreamClient,
|
|
4
|
+
PacketStreamServer,
|
|
5
|
+
TunnelManager,
|
|
6
|
+
} from './lib/tunnel/index.js';
|
|
7
|
+
import {
|
|
8
|
+
TunnelRegistryServer,
|
|
9
|
+
startTunnelRegistryServer,
|
|
10
|
+
} from './lib/tunnel/tunnel-registry-server.js';
|
|
11
|
+
import { Usbmux, createUsbmux } from './lib/usbmux/index.js';
|
|
12
|
+
import * as Services from './services.js';
|
|
13
|
+
import { startCoreDeviceProxy } from './services/ios/tunnel-service/index.js';
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
DiagnosticsService,
|
|
17
|
+
SyslogService,
|
|
18
|
+
SocketInfo,
|
|
19
|
+
TunnelResult,
|
|
20
|
+
TunnelRegistry,
|
|
21
|
+
TunnelRegistryEntry,
|
|
22
|
+
} from './lib/types.js';
|
|
23
|
+
export {
|
|
24
|
+
createUsbmux,
|
|
25
|
+
Services,
|
|
26
|
+
Usbmux,
|
|
27
|
+
TunnelManager,
|
|
28
|
+
PacketStreamServer,
|
|
29
|
+
PacketStreamClient,
|
|
30
|
+
createLockdownServiceByUDID,
|
|
31
|
+
startCoreDeviceProxy,
|
|
32
|
+
TunnelRegistryServer,
|
|
33
|
+
startTunnelRegistryServer,
|
|
34
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Default configuration values for Apple TV pairing behavior
|
|
2
|
+
export const DEFAULT_PAIRING_CONFIG = {
|
|
3
|
+
timeout: 30000,
|
|
4
|
+
discoveryTimeout: 5000,
|
|
5
|
+
maxRetries: 3,
|
|
6
|
+
pairingDirectory: '.pairing',
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
// TLV8 component type identifiers used in pairing data exchange
|
|
10
|
+
export const PairingDataComponentType = {
|
|
11
|
+
METHOD: 0x00,
|
|
12
|
+
IDENTIFIER: 0x01,
|
|
13
|
+
SALT: 0x02,
|
|
14
|
+
PUBLIC_KEY: 0x03,
|
|
15
|
+
PROOF: 0x04,
|
|
16
|
+
ENCRYPTED_DATA: 0x05,
|
|
17
|
+
STATE: 0x06,
|
|
18
|
+
ERROR: 0x07,
|
|
19
|
+
RETRY_DELAY: 0x08,
|
|
20
|
+
CERTIFICATE: 0x09,
|
|
21
|
+
SIGNATURE: 0x0a,
|
|
22
|
+
PERMISSIONS: 0x0b,
|
|
23
|
+
FRAGMENT_DATA: 0x0c,
|
|
24
|
+
FRAGMENT_LAST: 0x0d,
|
|
25
|
+
SESSION_ID: 0x0e,
|
|
26
|
+
TTL: 0x0f,
|
|
27
|
+
EXTRA_DATA: 0x10,
|
|
28
|
+
INFO: 0x11,
|
|
29
|
+
ACL: 0x12,
|
|
30
|
+
FLAGS: 0x13,
|
|
31
|
+
VALIDATION_DATA: 0x14,
|
|
32
|
+
MFI_AUTH_TOKEN: 0x15,
|
|
33
|
+
MFI_PRODUCT_TYPE: 0x16,
|
|
34
|
+
SERIAL_NUMBER: 0x17,
|
|
35
|
+
MFI_AUTH_TOKEN_UUID: 0x18,
|
|
36
|
+
APP_FLAGS: 0x19,
|
|
37
|
+
OWNERSHIP_PROOF: 0x1a,
|
|
38
|
+
SETUP_CODE_TYPE: 0x1b,
|
|
39
|
+
PRODUCTION_DATA: 0x1c,
|
|
40
|
+
APP_INFO: 0x1d,
|
|
41
|
+
SEPARATOR: 0xff,
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
// Maximum allowed size of a TLV8 fragment
|
|
45
|
+
export const TLV8_MAX_FRAGMENT_SIZE = 255;
|
|
46
|
+
|
|
47
|
+
// RFC 5054 3072-bit safe prime used for SRP key exchange
|
|
48
|
+
export const SRP_PRIME_3072 = BigInt(
|
|
49
|
+
'0x' +
|
|
50
|
+
'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' +
|
|
51
|
+
'020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' +
|
|
52
|
+
'4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' +
|
|
53
|
+
'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF05' +
|
|
54
|
+
'98DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB' +
|
|
55
|
+
'9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B' +
|
|
56
|
+
'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718' +
|
|
57
|
+
'3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33' +
|
|
58
|
+
'A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7' +
|
|
59
|
+
'ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864' +
|
|
60
|
+
'D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E2' +
|
|
61
|
+
'08E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF',
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Generator value (g=5) used in SRP key exchange
|
|
65
|
+
export const SRP_GENERATOR = BigInt(5);
|
|
66
|
+
|
|
67
|
+
// Hash algorithm used in SRP protocol (per HomeKit spec)
|
|
68
|
+
export const SRP_HASH_ALGORITHM = 'sha512';
|
|
69
|
+
|
|
70
|
+
// SRP username identifier used in Apple Pair-Setup
|
|
71
|
+
export const SRP_USERNAME = 'Pair-Setup';
|
|
72
|
+
|
|
73
|
+
// Key length in bytes for SRP (3072 bits = 384 bytes)
|
|
74
|
+
export const SRP_KEY_LENGTH_BYTES = 384;
|
|
75
|
+
|
|
76
|
+
// Number of bits for SRP private key (usually 256 bits)
|
|
77
|
+
export const SRP_PRIVATE_KEY_BITS = 256;
|
|
78
|
+
|
|
79
|
+
// Hash algorithm used for HKDF in pairing encryption
|
|
80
|
+
export const HKDF_HASH_ALGORITHM = 'sha512';
|
|
81
|
+
|
|
82
|
+
// Output length (in bytes) for HKDF key derivation
|
|
83
|
+
export const HKDF_HASH_LENGTH = 64;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Base error class for all Apple TV related errors
|
|
2
|
+
export class AppleTVError extends Error {
|
|
3
|
+
constructor(message: string) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Represents an error that occurs during the pairing process
|
|
11
|
+
export class PairingError extends AppleTVError {
|
|
12
|
+
constructor(
|
|
13
|
+
message: string,
|
|
14
|
+
public code?: string,
|
|
15
|
+
public details?: any,
|
|
16
|
+
) {
|
|
17
|
+
super(message);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Represents an error related to network communication
|
|
22
|
+
export class NetworkError extends AppleTVError {}
|
|
23
|
+
|
|
24
|
+
// Represents an error occurring during cryptographic operations
|
|
25
|
+
export class CryptographyError extends AppleTVError {}
|
|
26
|
+
|
|
27
|
+
// Represents an error specific to SRP (Secure Remote Password) protocol
|
|
28
|
+
export class SRPError extends AppleTVError {}
|
|
29
|
+
|
|
30
|
+
// Represents an error related to TLV8 encoding/decoding
|
|
31
|
+
export class TLV8Error extends AppleTVError {}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { TLV8Error } from '../errors.js';
|
|
2
|
+
import type { PairingDataComponentTypeValue, TLV8Item } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Decodes a TLV8-formatted buffer into an array of TLV8 items.
|
|
6
|
+
*
|
|
7
|
+
* @param buffer - A Node.js Buffer containing TLV8 encoded data
|
|
8
|
+
* @returns Array of TLV8Item objects with `type` and `data`
|
|
9
|
+
* @throws TLV8Error if the buffer does not contain valid TLV8 data
|
|
10
|
+
*/
|
|
11
|
+
export function decodeTLV8(buffer: Buffer): TLV8Item[] {
|
|
12
|
+
const items: TLV8Item[] = [];
|
|
13
|
+
let offset = 0;
|
|
14
|
+
|
|
15
|
+
while (offset < buffer.length) {
|
|
16
|
+
if (offset + 2 > buffer.length) {
|
|
17
|
+
throw new TLV8Error(
|
|
18
|
+
`Invalid TLV8: insufficient data for type and length at offset ${offset}`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const type = buffer[offset] as PairingDataComponentTypeValue;
|
|
23
|
+
const length = buffer[offset + 1];
|
|
24
|
+
offset += 2;
|
|
25
|
+
|
|
26
|
+
if (offset + length > buffer.length) {
|
|
27
|
+
throw new TLV8Error(
|
|
28
|
+
`Invalid TLV8: insufficient data for value at offset ${offset}`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const data = buffer.subarray(offset, offset + length);
|
|
33
|
+
offset += length;
|
|
34
|
+
|
|
35
|
+
items.push({ type, data });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return items;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Decodes a TLV8-formatted buffer into a dictionary mapping
|
|
43
|
+
* each TLV8 type to its corresponding data buffer. If the same
|
|
44
|
+
* type occurs more than once, their values are concatenated.
|
|
45
|
+
*
|
|
46
|
+
* @param buffer - A Node.js Buffer containing TLV8 encoded data
|
|
47
|
+
* @returns A dictionary of type-value mappings
|
|
48
|
+
*/
|
|
49
|
+
export function decodeTLV8ToDict(
|
|
50
|
+
buffer: Buffer,
|
|
51
|
+
): Partial<Record<PairingDataComponentTypeValue, Buffer>> {
|
|
52
|
+
const items = decodeTLV8(buffer);
|
|
53
|
+
const result: Partial<Record<PairingDataComponentTypeValue, Buffer[]>> = {};
|
|
54
|
+
|
|
55
|
+
for (const { type, data } of items) {
|
|
56
|
+
if (!result[type]) {
|
|
57
|
+
result[type] = [];
|
|
58
|
+
}
|
|
59
|
+
result[type]!.push(data);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Object.fromEntries(
|
|
63
|
+
Object.entries(result).map(([type, buffers]) => [
|
|
64
|
+
type,
|
|
65
|
+
Buffer.concat(buffers as Buffer[]),
|
|
66
|
+
]),
|
|
67
|
+
) as Partial<Record<PairingDataComponentTypeValue, Buffer>>;
|
|
68
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TLV8_MAX_FRAGMENT_SIZE } from '../constants.js';
|
|
2
|
+
import type { TLV8Item } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Encodes an array of TLV8 items into a single TLV8-compliant buffer.
|
|
6
|
+
* If a data value exceeds TLV8_MAX_FRAGMENT_SIZE, it will be split across multiple entries.
|
|
7
|
+
*
|
|
8
|
+
* @param items - Array of TLV8 items to encode
|
|
9
|
+
* @returns A Buffer containing the encoded TLV8 data
|
|
10
|
+
*/
|
|
11
|
+
export function encodeTLV8(items: TLV8Item[]): Buffer {
|
|
12
|
+
const chunks: Buffer[] = [];
|
|
13
|
+
|
|
14
|
+
for (const { type, data } of items) {
|
|
15
|
+
let offset = 0;
|
|
16
|
+
|
|
17
|
+
while (offset < data.length) {
|
|
18
|
+
const fragmentLength = Math.min(
|
|
19
|
+
TLV8_MAX_FRAGMENT_SIZE,
|
|
20
|
+
data.length - offset,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
chunks.push(
|
|
24
|
+
Buffer.from([type, fragmentLength]),
|
|
25
|
+
data.subarray(offset, offset + fragmentLength),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
offset += fragmentLength;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return Buffer.concat(chunks);
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PairingDataComponentType } from '../constants.js';
|
|
2
|
+
import { encodeTLV8 } from './encoder.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates TLV8-encoded setup data for manual pairing, with default METHOD and STATE.
|
|
6
|
+
*
|
|
7
|
+
* @returns Base64-encoded TLV8 string for manual pairing
|
|
8
|
+
*/
|
|
9
|
+
export function createSetupManualPairingData(): string {
|
|
10
|
+
const tlv = encodeTLV8([
|
|
11
|
+
{ type: PairingDataComponentType.METHOD, data: Buffer.from([0x00]) },
|
|
12
|
+
{ type: PairingDataComponentType.STATE, data: Buffer.from([0x01]) },
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
return tlv.toString('base64');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates TLV8-encoded data for pair verification, including the X25519 public key.
|
|
20
|
+
*
|
|
21
|
+
* @param x25519PublicKey - A buffer containing the X25519 public key
|
|
22
|
+
* @returns Base64-encoded TLV8 string for verification
|
|
23
|
+
*/
|
|
24
|
+
export function createPairVerificationData(x25519PublicKey: Buffer): string {
|
|
25
|
+
const tlv = encodeTLV8([
|
|
26
|
+
{ type: PairingDataComponentType.STATE, data: Buffer.from([0x01]) },
|
|
27
|
+
{ type: PairingDataComponentType.PUBLIC_KEY, data: x25519PublicKey },
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
return tlv.toString('base64');
|
|
31
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Represents detailed information about an Apple TV device
|
|
2
|
+
export interface AppleTVDeviceInfo {
|
|
3
|
+
altIRK: Buffer;
|
|
4
|
+
btAddr: string;
|
|
5
|
+
mac: Buffer;
|
|
6
|
+
remotePairingSerialNumber: string;
|
|
7
|
+
accountID: string;
|
|
8
|
+
model: string;
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Represents a key pair used during pairing (public/private keys)
|
|
13
|
+
export interface PairingKeys {
|
|
14
|
+
publicKey: Buffer;
|
|
15
|
+
privateKey: Buffer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Represents the result of a pairing attempt
|
|
19
|
+
export interface PairingResult {
|
|
20
|
+
success: boolean;
|
|
21
|
+
pairingFile?: string;
|
|
22
|
+
deviceId: string;
|
|
23
|
+
error?: Error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Configuration options for the pairing process
|
|
27
|
+
export interface PairingConfig {
|
|
28
|
+
timeout: number;
|
|
29
|
+
discoveryTimeout: number;
|
|
30
|
+
maxRetries: number;
|
|
31
|
+
pairingDirectory: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Represents a TLV8 data item with a type and binary data
|
|
35
|
+
export interface TLV8Item {
|
|
36
|
+
type: PairingDataComponentTypeValue;
|
|
37
|
+
data: Buffer;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Type alias for TLV8 component type values
|
|
41
|
+
export type PairingDataComponentTypeValue = number;
|
|
42
|
+
|
|
43
|
+
// Represents any valid Opack2 data type
|
|
44
|
+
export type Opack2Value =
|
|
45
|
+
| null
|
|
46
|
+
| undefined
|
|
47
|
+
| boolean
|
|
48
|
+
| number
|
|
49
|
+
| string
|
|
50
|
+
| Buffer
|
|
51
|
+
| Opack2Array
|
|
52
|
+
| Opack2Dictionary;
|
|
53
|
+
|
|
54
|
+
// Represents an array of Opack2 values
|
|
55
|
+
export interface Opack2Array extends Array<Opack2Value> {}
|
|
56
|
+
|
|
57
|
+
// Represents a dictionary of Opack2 values
|
|
58
|
+
export interface Opack2Dictionary extends Record<string, Opack2Value> {}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a non-negative bigint to a fixed-length Buffer in big-endian format.
|
|
3
|
+
*
|
|
4
|
+
* @param value - The bigint value to convert (must be non-negative).
|
|
5
|
+
* @param length - The target buffer length in bytes.
|
|
6
|
+
* @returns A Buffer representing the bigint, padded to the specified length.
|
|
7
|
+
*
|
|
8
|
+
* @throws {RangeError} If the value is negative or doesn't fit in the specified length.
|
|
9
|
+
*/
|
|
10
|
+
export function bigIntToBuffer(value: bigint, length: number): Buffer {
|
|
11
|
+
if (value < 0n) {
|
|
12
|
+
throw new RangeError('Negative values not supported');
|
|
13
|
+
}
|
|
14
|
+
const hex = value.toString(16);
|
|
15
|
+
const byteLength = Math.ceil(hex.length / 2);
|
|
16
|
+
|
|
17
|
+
if (byteLength > length) {
|
|
18
|
+
throw new RangeError(
|
|
19
|
+
`Value 0x${hex} is too large to fit in ${length} bytes`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const paddedHex = hex.padStart(length * 2, '0');
|
|
24
|
+
return Buffer.from(paddedHex, 'hex');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Converts a Buffer into a bigint by interpreting it as a big-endian hexadecimal number.
|
|
29
|
+
*
|
|
30
|
+
* @param buffer - The input Buffer.
|
|
31
|
+
* @returns A bigint representing the numeric value of the buffer.
|
|
32
|
+
*/
|
|
33
|
+
export function bufferToBigInt(buffer: Buffer): bigint {
|
|
34
|
+
return BigInt('0x' + buffer.toString('hex'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Converts a non-negative bigint into a minimal-length Buffer in big-endian format.
|
|
39
|
+
* No unnecessary leading zero bytes will be included.
|
|
40
|
+
*
|
|
41
|
+
* @param value - The bigint value to convert (must be non-negative).
|
|
42
|
+
* @returns A Buffer representing the bigint with minimal byte length.
|
|
43
|
+
*
|
|
44
|
+
* @throws {RangeError} If the value is negative.
|
|
45
|
+
*/
|
|
46
|
+
export function bigIntToMinimalBuffer(value: bigint): Buffer {
|
|
47
|
+
if (value < 0n) {
|
|
48
|
+
throw new RangeError('Negative values not supported');
|
|
49
|
+
}
|
|
50
|
+
const hex = value.toString(16);
|
|
51
|
+
const paddedHex = hex.length % 2 === 0 ? hex : '0' + hex;
|
|
52
|
+
return Buffer.from(paddedHex, 'hex');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Computes modular exponentiation: (base ^ exponent) % modulus.
|
|
57
|
+
* Efficiently handles large numbers using the binary exponentiation method.
|
|
58
|
+
*
|
|
59
|
+
* @param base - The base number.
|
|
60
|
+
* @param exponent - The exponent (must be non-negative).
|
|
61
|
+
* @param modulus - The modulus (must be non-zero).
|
|
62
|
+
* @returns The result of (base ** exponent) modulo modulus.
|
|
63
|
+
*
|
|
64
|
+
* @throws {RangeError} If the exponent is negative or the modulus is zero.
|
|
65
|
+
*/
|
|
66
|
+
export function modPow(
|
|
67
|
+
base: bigint,
|
|
68
|
+
exponent: bigint,
|
|
69
|
+
modulus: bigint,
|
|
70
|
+
): bigint {
|
|
71
|
+
if (modulus === 0n) {
|
|
72
|
+
throw new RangeError('Modulus must be non-zero');
|
|
73
|
+
}
|
|
74
|
+
if (exponent < 0n) {
|
|
75
|
+
throw new RangeError('Negative exponents not supported');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let result = 1n;
|
|
79
|
+
base = base % modulus;
|
|
80
|
+
|
|
81
|
+
while (exponent > 0n) {
|
|
82
|
+
if (exponent % 2n === 1n) {
|
|
83
|
+
result = (result * base) % modulus;
|
|
84
|
+
}
|
|
85
|
+
exponent = exponent / 2n;
|
|
86
|
+
base = (base * base) % modulus;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
// UUID namespace for DNS, per RFC 4122
|
|
4
|
+
const NAMESPACE_DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generates a deterministic UUIDv3 (MD5-based) from a hostname string.
|
|
8
|
+
* This mimics DNS-based UUID generation using the standard DNS namespace UUID.
|
|
9
|
+
*
|
|
10
|
+
* @param hostname - The host string to be converted to a UUID
|
|
11
|
+
* @returns A UUIDv3 string in uppercase
|
|
12
|
+
*/
|
|
13
|
+
export function generateHostId(hostname: string): string {
|
|
14
|
+
if (typeof hostname !== 'string' || hostname.length === 0) {
|
|
15
|
+
throw new TypeError('Hostname must be a non-empty string');
|
|
16
|
+
}
|
|
17
|
+
const namespaceBytes = Buffer.from(NAMESPACE_DNS.replace(/-/g, ''), 'hex');
|
|
18
|
+
|
|
19
|
+
// Hash the namespace and the hostname using MD5
|
|
20
|
+
const hash = createHash('md5');
|
|
21
|
+
hash.update(namespaceBytes);
|
|
22
|
+
hash.update(hostname, 'utf8');
|
|
23
|
+
const hashBytes = hash.digest();
|
|
24
|
+
|
|
25
|
+
// Set UUID version to 3 (MD5)
|
|
26
|
+
hashBytes[6] = (hashBytes[6] & 0x0f) | 0x30;
|
|
27
|
+
|
|
28
|
+
// Set UUID variant to RFC 4122
|
|
29
|
+
hashBytes[8] = (hashBytes[8] & 0x3f) | 0x80;
|
|
30
|
+
|
|
31
|
+
// Convert to UUID string format
|
|
32
|
+
const uuid = [
|
|
33
|
+
hashBytes.subarray(0, 4).toString('hex'),
|
|
34
|
+
hashBytes.subarray(4, 6).toString('hex'),
|
|
35
|
+
hashBytes.subarray(6, 8).toString('hex'),
|
|
36
|
+
hashBytes.subarray(8, 10).toString('hex'),
|
|
37
|
+
hashBytes.subarray(10, 16).toString('hex'),
|
|
38
|
+
]
|
|
39
|
+
.join('-')
|
|
40
|
+
.toUpperCase();
|
|
41
|
+
|
|
42
|
+
return uuid;
|
|
43
|
+
}
|