@basmilius/apple-common 0.10.1 → 0.12.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/dist/index.d.mts +72 -64
- package/dist/index.mjs +231 -101
- package/package.json +4 -4
package/dist/index.d.mts
CHANGED
|
@@ -13,61 +13,22 @@ declare function v4(options?: Version4Options, buf?: undefined, offset?: number)
|
|
|
13
13
|
declare function v4<TBuf extends Uint8Array = Uint8Array>(options: Version4Options | undefined, buf: TBuf, offset?: number): TBuf;
|
|
14
14
|
//#endregion
|
|
15
15
|
//#region src/airplayFeatures.d.ts
|
|
16
|
-
/** Type definition for the AirPlay feature flags bitmask object. */
|
|
17
|
-
type AirPlayFeatureFlagsType = {
|
|
18
|
-
readonly SupportsAirPlayVideoV1: bigint;
|
|
19
|
-
readonly SupportsAirPlayPhoto: bigint;
|
|
20
|
-
readonly SupportsAirPlayVideoFairPlay: bigint;
|
|
21
|
-
readonly SupportsAirPlayVideoVolumeControl: bigint;
|
|
22
|
-
readonly SupportsAirPlayVideoHTTPLiveStreams: bigint;
|
|
23
|
-
readonly SupportsAirPlaySlideShow: bigint;
|
|
24
|
-
readonly SupportsAirPlayScreen: bigint;
|
|
25
|
-
readonly SupportsAirPlayAudio: bigint;
|
|
26
|
-
readonly AudioRedundant: bigint;
|
|
27
|
-
readonly Authentication_4: bigint;
|
|
28
|
-
readonly MetadataFeatures_0: bigint;
|
|
29
|
-
readonly MetadataFeatures_1: bigint;
|
|
30
|
-
readonly MetadataFeatures_2: bigint;
|
|
31
|
-
readonly AudioFormats_0: bigint;
|
|
32
|
-
readonly AudioFormats_1: bigint;
|
|
33
|
-
readonly AudioFormats_2: bigint;
|
|
34
|
-
readonly AudioFormats_3: bigint;
|
|
35
|
-
readonly Authentication_1: bigint;
|
|
36
|
-
readonly Authentication_8: bigint;
|
|
37
|
-
readonly SupportsLegacyPairing: bigint;
|
|
38
|
-
readonly HasUnifiedAdvertiserInfo: bigint;
|
|
39
|
-
readonly IsCarPlay: bigint;
|
|
40
|
-
readonly SupportsAirPlayVideoPlayQueue: bigint;
|
|
41
|
-
readonly SupportsAirPlayFromCloud: bigint;
|
|
42
|
-
readonly SupportsTLS_PSK: bigint;
|
|
43
|
-
readonly SupportsUnifiedMediaControl: bigint;
|
|
44
|
-
readonly SupportsBufferedAudio: bigint;
|
|
45
|
-
readonly SupportsPTP: bigint;
|
|
46
|
-
readonly SupportsScreenMultiCodec: bigint;
|
|
47
|
-
readonly SupportsSystemPairing: bigint;
|
|
48
|
-
readonly IsAPValeriaScreenSender: bigint;
|
|
49
|
-
readonly SupportsHKPairingAndAccessControl: bigint;
|
|
50
|
-
readonly SupportsCoreUtilsPairingAndEncryption: bigint;
|
|
51
|
-
readonly SupportsAirPlayVideoV2: bigint;
|
|
52
|
-
readonly MetadataFeatures_3: bigint;
|
|
53
|
-
readonly SupportsUnifiedPairSetupAndMFi: bigint;
|
|
54
|
-
readonly SupportsSetPeersExtendedMessage: bigint;
|
|
55
|
-
readonly SupportsAPSync: bigint;
|
|
56
|
-
readonly SupportsWoL: bigint;
|
|
57
|
-
readonly SupportsWoL2: bigint;
|
|
58
|
-
readonly SupportsHangdogRemoteControl: bigint;
|
|
59
|
-
readonly SupportsAudioStreamConnectionSetup: bigint;
|
|
60
|
-
readonly SupportsAudioMetadataControl: bigint;
|
|
61
|
-
readonly SupportsRFC2198Redundancy: bigint;
|
|
62
|
-
};
|
|
63
16
|
/**
|
|
64
17
|
* AirPlay feature flags as a bitmask of bigint values. Each flag corresponds to a specific
|
|
65
18
|
* capability advertised by an AirPlay device in its mDNS TXT record "features" field.
|
|
66
|
-
*
|
|
19
|
+
*
|
|
20
|
+
* Bit positions are derived from Apple's internal `APSFeaturesSetFeature()` calls in the
|
|
21
|
+
* AirPlayReceiver framework (`sysInfo_createFeaturesInternal`). Flag names follow the
|
|
22
|
+
* community convention (pyatv, emanuelecozzi.net) with camelCase normalization.
|
|
23
|
+
*
|
|
24
|
+
* Sources:
|
|
25
|
+
* - Apple AirPlayReceiver framework decompilation (sysInfo.c)
|
|
26
|
+
* - pyatv AirPlayFlags enum (pyatv/protocols/airplay/utils.py)
|
|
27
|
+
* - https://emanuelecozzi.net/docs/airplay2/features/
|
|
67
28
|
*/
|
|
68
|
-
declare const AirPlayFeatureFlags:
|
|
69
|
-
/** String
|
|
70
|
-
type AirPlayFeatureFlagName =
|
|
29
|
+
declare const AirPlayFeatureFlags: Record<string, bigint>;
|
|
30
|
+
/** String name of any known AirPlay feature flag. */
|
|
31
|
+
type AirPlayFeatureFlagName = string;
|
|
71
32
|
/** The type of pairing required to connect to an AirPlay device. */
|
|
72
33
|
type PairingRequirement = 'none' | 'pin' | 'transient' | 'homekit';
|
|
73
34
|
/**
|
|
@@ -130,6 +91,21 @@ declare function isPasswordRequired(txt: Record<string, string>): boolean;
|
|
|
130
91
|
* @returns True if remote control is supported (typically Apple TV only).
|
|
131
92
|
*/
|
|
132
93
|
declare function isRemoteControlSupported(txt: Record<string, string>): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Feature bitmask advertised when connecting for remote control sessions.
|
|
96
|
+
*
|
|
97
|
+
* Includes media control, system pairing, encryption, volume, and
|
|
98
|
+
* hangdog remote control capabilities.
|
|
99
|
+
*/
|
|
100
|
+
declare const SENDER_FEATURES_REMOTE_CONTROL: bigint;
|
|
101
|
+
/**
|
|
102
|
+
* Feature bitmask advertised when connecting for audio streaming sessions.
|
|
103
|
+
*
|
|
104
|
+
* Extends the remote control features with buffered audio, audio stream
|
|
105
|
+
* connection setup, metadata control, format negotiation, and PTP
|
|
106
|
+
* synchronization support.
|
|
107
|
+
*/
|
|
108
|
+
declare const SENDER_FEATURES_AUDIO: bigint;
|
|
133
109
|
//#endregion
|
|
134
110
|
//#region src/reporter.d.ts
|
|
135
111
|
/**
|
|
@@ -298,7 +274,12 @@ declare class AccessoryPair extends BasePairing {
|
|
|
298
274
|
* @param context - Shared context for logging and device identity.
|
|
299
275
|
* @param requestHandler - Callback that sends TLV8-encoded data and returns the response.
|
|
300
276
|
*/
|
|
301
|
-
|
|
277
|
+
/**
|
|
278
|
+
* @param context - Shared context for logging and device identity.
|
|
279
|
+
* @param requestHandler - Callback that sends TLV8-encoded data and returns the response.
|
|
280
|
+
* @param useAes - Use AES-128-CTR instead of ChaCha20-Poly1305 for M5/M6 encryption (legacy devices).
|
|
281
|
+
*/
|
|
282
|
+
constructor(context: Context, requestHandler: RequestHandler, useAes?: boolean);
|
|
302
283
|
/** Generates a new Ed25519 key pair for long-term identity. Must be called before pin() or transient(). */
|
|
303
284
|
start(): Promise<void>;
|
|
304
285
|
/**
|
|
@@ -385,8 +366,9 @@ declare class AccessoryVerify extends BasePairing {
|
|
|
385
366
|
/**
|
|
386
367
|
* @param context - Shared context for logging and device identity.
|
|
387
368
|
* @param requestHandler - Callback that sends TLV8-encoded data and returns the response.
|
|
369
|
+
* @param useAes - Use AES-128-CTR instead of ChaCha20-Poly1305 for encryption (legacy devices).
|
|
388
370
|
*/
|
|
389
|
-
constructor(context: Context, requestHandler: RequestHandler);
|
|
371
|
+
constructor(context: Context, requestHandler: RequestHandler, useAes?: boolean);
|
|
390
372
|
/**
|
|
391
373
|
* Performs the full pair-verify flow (M1 through M4) using stored credentials
|
|
392
374
|
* from a previous pair-setup. Establishes a new session with forward secrecy
|
|
@@ -962,10 +944,20 @@ declare class EncryptionState {
|
|
|
962
944
|
}
|
|
963
945
|
//#endregion
|
|
964
946
|
//#region src/const.d.ts
|
|
947
|
+
/** Default number of bytes per audio channel sample (16-bit PCM). */
|
|
948
|
+
declare const AUDIO_BYTES_PER_CHANNEL = 2;
|
|
949
|
+
/** Default number of audio channels (stereo). */
|
|
950
|
+
declare const AUDIO_CHANNELS = 2;
|
|
951
|
+
/** Number of PCM audio frames packed into a single RTP packet (ALAC/RAOP standard). */
|
|
952
|
+
declare const AUDIO_FRAMES_PER_PACKET = 352;
|
|
953
|
+
/** Default audio sample rate in Hz (CD quality). */
|
|
954
|
+
declare const AUDIO_SAMPLE_RATE = 44100;
|
|
965
955
|
/** Default PIN used for transient (non-persistent) AirPlay pairing sessions. */
|
|
966
956
|
declare const AIRPLAY_TRANSIENT_PIN = "3939";
|
|
967
957
|
/** Timeout in milliseconds for HTTP requests during setup and control. */
|
|
968
958
|
declare const HTTP_TIMEOUT = 6000;
|
|
959
|
+
/** Timeout in milliseconds for TCP socket connections during initial connect. Matches Apple's 30s default. */
|
|
960
|
+
declare const SOCKET_TIMEOUT = 30000;
|
|
969
961
|
/** mDNS service type for AirPlay device discovery. */
|
|
970
962
|
declare const AIRPLAY_SERVICE = "_airplay._tcp.local";
|
|
971
963
|
/** mDNS service type for Companion Link (remote control protocol) device discovery. */
|
|
@@ -973,6 +965,26 @@ declare const COMPANION_LINK_SERVICE = "_companion-link._tcp.local";
|
|
|
973
965
|
/** mDNS service type for RAOP (Remote Audio Output Protocol) device discovery. */
|
|
974
966
|
declare const RAOP_SERVICE = "_raop._tcp.local";
|
|
975
967
|
//#endregion
|
|
968
|
+
//#region src/hkdf.d.ts
|
|
969
|
+
/**
|
|
970
|
+
* Derives a pair of ChaCha20 encryption keys (read + write) from a shared secret
|
|
971
|
+
* using HKDF-SHA512 with direction-specific info strings.
|
|
972
|
+
*
|
|
973
|
+
* This is a shared helper used across AirPlay, Companion Link, and RAOP pairing
|
|
974
|
+
* flows to eliminate repeated HKDF boilerplate. The salt and info strings vary
|
|
975
|
+
* per protocol and stream type.
|
|
976
|
+
*
|
|
977
|
+
* @param sharedSecret - The shared secret from a pair-verify or pair-setup flow.
|
|
978
|
+
* @param salt - HKDF salt string (protocol-specific, e.g. 'Control-Salt').
|
|
979
|
+
* @param readInfo - HKDF info string for the read (decrypt) key.
|
|
980
|
+
* @param writeInfo - HKDF info string for the write (encrypt) key.
|
|
981
|
+
* @returns An object with `readKey` and `writeKey` as 32-byte Buffers.
|
|
982
|
+
*/
|
|
983
|
+
declare function deriveEncryptionKeys(sharedSecret: Buffer, salt: string, readInfo: string, writeInfo: string): {
|
|
984
|
+
readKey: Buffer;
|
|
985
|
+
writeKey: Buffer;
|
|
986
|
+
};
|
|
987
|
+
//#endregion
|
|
976
988
|
//#region src/symbols.d.ts
|
|
977
989
|
/**
|
|
978
990
|
* Unique symbol used as a key for accessing encryption state on connection instances.
|
|
@@ -1056,10 +1068,6 @@ declare class TimingServer {
|
|
|
1056
1068
|
* @throws If the socket fails to bind.
|
|
1057
1069
|
*/
|
|
1058
1070
|
listen(): Promise<void>;
|
|
1059
|
-
/**
|
|
1060
|
-
* Handles the socket 'connect' event by configuring buffer sizes.
|
|
1061
|
-
*/
|
|
1062
|
-
onConnect(): void;
|
|
1063
1071
|
/**
|
|
1064
1072
|
* Handles socket errors by logging them.
|
|
1065
1073
|
*
|
|
@@ -1167,42 +1175,42 @@ declare enum DeviceType {
|
|
|
1167
1175
|
* @param identifier - Model identifier (e.g. "AppleTV6,2") or internal name (e.g. "J305AP").
|
|
1168
1176
|
* @returns The matching device model, or {@link DeviceModel.Unknown} if unrecognized.
|
|
1169
1177
|
*/
|
|
1170
|
-
declare
|
|
1178
|
+
declare function lookupDeviceModel(identifier: string): DeviceModel;
|
|
1171
1179
|
/**
|
|
1172
1180
|
* Returns the human-readable display name for a device model.
|
|
1173
1181
|
*
|
|
1174
1182
|
* @param model - The device model to look up.
|
|
1175
1183
|
* @returns A display name like "Apple TV 4K (2nd generation)", or "Unknown".
|
|
1176
1184
|
*/
|
|
1177
|
-
declare
|
|
1185
|
+
declare function getDeviceModelName(model: DeviceModel): string;
|
|
1178
1186
|
/**
|
|
1179
1187
|
* Returns the high-level device type category for a device model.
|
|
1180
1188
|
*
|
|
1181
1189
|
* @param model - The device model to categorize.
|
|
1182
1190
|
* @returns The device type (AppleTV, HomePod, AirPort, or Unknown).
|
|
1183
1191
|
*/
|
|
1184
|
-
declare
|
|
1192
|
+
declare function getDeviceType(model: DeviceModel): DeviceType;
|
|
1185
1193
|
/**
|
|
1186
1194
|
* Checks whether the given model is an Apple TV.
|
|
1187
1195
|
*
|
|
1188
1196
|
* @param model - The device model to check.
|
|
1189
1197
|
* @returns True if the model is any Apple TV generation.
|
|
1190
1198
|
*/
|
|
1191
|
-
declare
|
|
1199
|
+
declare function isAppleTV(model: DeviceModel): boolean;
|
|
1192
1200
|
/**
|
|
1193
1201
|
* Checks whether the given model is a HomePod.
|
|
1194
1202
|
*
|
|
1195
1203
|
* @param model - The device model to check.
|
|
1196
1204
|
* @returns True if the model is any HomePod variant.
|
|
1197
1205
|
*/
|
|
1198
|
-
declare
|
|
1206
|
+
declare function isHomePod(model: DeviceModel): boolean;
|
|
1199
1207
|
/**
|
|
1200
1208
|
* Checks whether the given model is an AirPort Express.
|
|
1201
1209
|
*
|
|
1202
1210
|
* @param model - The device model to check.
|
|
1203
1211
|
* @returns True if the model is any AirPort Express generation.
|
|
1204
1212
|
*/
|
|
1205
|
-
declare
|
|
1213
|
+
declare function isAirPort(model: DeviceModel): boolean;
|
|
1206
1214
|
//#endregion
|
|
1207
1215
|
//#region src/errors.d.ts
|
|
1208
1216
|
/**
|
|
@@ -1315,4 +1323,4 @@ interface AudioSource {
|
|
|
1315
1323
|
stop(): Promise<void>;
|
|
1316
1324
|
}
|
|
1317
1325
|
//#endregion
|
|
1318
|
-
export { AIRPLAY_SERVICE, AIRPLAY_TRANSIENT_PIN, type AccessoryCredentials, type AccessoryKeys, AccessoryPair, AccessoryVerify, type AirPlayFeatureFlagName, AirPlayFeatureFlags, AppleProtocolError, type AudioSource, AuthenticationError, COMPANION_LINK_SERVICE, type CombinedDiscoveryResult, CommandError, Connection, ConnectionClosedError, ConnectionError, ConnectionRecovery, type ConnectionRecoveryOptions, type ConnectionState, ConnectionTimeoutError, Context, CredentialsError, type DeviceIdentity, DeviceModel, DeviceType, Discovery, DiscoveryError, type DiscoveryResult, ENCRYPTION, EncryptionAwareConnection, EncryptionError, EncryptionState, type EventMap, HTTP_TIMEOUT, InvalidResponseError, JsonStorage, type Logger, type MdnsService, MemoryStorage, PairingError, type PairingRequirement, PlaybackError, type ProtocolType, RAOP_SERVICE, type Reporter, SetupError, Storage, type StorageData, type StoredDevice, TimeoutError, TimingServer, describeFlags, generateActiveRemoteId, generateDacpId, generateSessionId, getDeviceModelName, getDeviceType, getLocalIP, getMacAddress, getPairingRequirement, getProtocolVersion, hasFeatureFlag, isAirPort, isAppleTV, isHomePod, isPasswordRequired, isRemoteControlSupported, lookupDeviceModel, multicast as mdnsMulticast, unicast as mdnsUnicast, parseFeatures, prompt, randomInt32, randomInt64, reporter, uint16ToBE, uint53ToLE, v4 as uuid, waitFor };
|
|
1326
|
+
export { AIRPLAY_SERVICE, AIRPLAY_TRANSIENT_PIN, AUDIO_BYTES_PER_CHANNEL, AUDIO_CHANNELS, AUDIO_FRAMES_PER_PACKET, AUDIO_SAMPLE_RATE, type AccessoryCredentials, type AccessoryKeys, AccessoryPair, AccessoryVerify, type AirPlayFeatureFlagName, AirPlayFeatureFlags, AppleProtocolError, type AudioSource, AuthenticationError, COMPANION_LINK_SERVICE, type CombinedDiscoveryResult, CommandError, Connection, ConnectionClosedError, ConnectionError, ConnectionRecovery, type ConnectionRecoveryOptions, type ConnectionState, ConnectionTimeoutError, Context, CredentialsError, type DeviceIdentity, DeviceModel, DeviceType, Discovery, DiscoveryError, type DiscoveryResult, ENCRYPTION, EncryptionAwareConnection, EncryptionError, EncryptionState, type EventMap, HTTP_TIMEOUT, InvalidResponseError, JsonStorage, type Logger, type MdnsService, MemoryStorage, PairingError, type PairingRequirement, PlaybackError, type ProtocolType, RAOP_SERVICE, type Reporter, SENDER_FEATURES_AUDIO, SENDER_FEATURES_REMOTE_CONTROL, SOCKET_TIMEOUT, SetupError, Storage, type StorageData, type StoredDevice, TimeoutError, TimingServer, deriveEncryptionKeys, describeFlags, generateActiveRemoteId, generateDacpId, generateSessionId, getDeviceModelName, getDeviceType, getLocalIP, getMacAddress, getPairingRequirement, getProtocolVersion, hasFeatureFlag, isAirPort, isAppleTV, isHomePod, isPasswordRequired, isRemoteControlSupported, lookupDeviceModel, multicast as mdnsMulticast, unicast as mdnsUnicast, parseFeatures, prompt, randomInt32, randomInt64, reporter, uint16ToBE, uint53ToLE, v4 as uuid, waitFor };
|
package/dist/index.mjs
CHANGED
|
@@ -2,13 +2,13 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { randomBytes, randomFillSync, randomUUID } from "node:crypto";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import { Socket, createConnection } from "node:net";
|
|
6
5
|
import { createInterface } from "node:readline";
|
|
7
6
|
import { createSocket } from "node:dgram";
|
|
7
|
+
import { Socket, createConnection } from "node:net";
|
|
8
8
|
import { networkInterfaces } from "node:os";
|
|
9
9
|
import { EventEmitter } from "node:events";
|
|
10
|
+
import { Aes, Chacha20, Curve25519, Ed25519, hkdf } from "@basmilius/apple-encryption";
|
|
10
11
|
import { NTP, OPack, TLV8 } from "@basmilius/apple-encoding";
|
|
11
|
-
import { Chacha20, Curve25519, Ed25519, hkdf } from "@basmilius/apple-encryption";
|
|
12
12
|
|
|
13
13
|
//#region \0rolldown/runtime.js
|
|
14
14
|
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
@@ -64,7 +64,15 @@ function v4(options, buf, offset) {
|
|
|
64
64
|
/**
|
|
65
65
|
* AirPlay feature flags as a bitmask of bigint values. Each flag corresponds to a specific
|
|
66
66
|
* capability advertised by an AirPlay device in its mDNS TXT record "features" field.
|
|
67
|
-
*
|
|
67
|
+
*
|
|
68
|
+
* Bit positions are derived from Apple's internal `APSFeaturesSetFeature()` calls in the
|
|
69
|
+
* AirPlayReceiver framework (`sysInfo_createFeaturesInternal`). Flag names follow the
|
|
70
|
+
* community convention (pyatv, emanuelecozzi.net) with camelCase normalization.
|
|
71
|
+
*
|
|
72
|
+
* Sources:
|
|
73
|
+
* - Apple AirPlayReceiver framework decompilation (sysInfo.c)
|
|
74
|
+
* - pyatv AirPlayFlags enum (pyatv/protocols/airplay/utils.py)
|
|
75
|
+
* - https://emanuelecozzi.net/docs/airplay2/features/
|
|
68
76
|
*/
|
|
69
77
|
const AirPlayFeatureFlags = {
|
|
70
78
|
SupportsAirPlayVideoV1: 1n << 0n,
|
|
@@ -76,22 +84,22 @@ const AirPlayFeatureFlags = {
|
|
|
76
84
|
SupportsAirPlayScreen: 1n << 7n,
|
|
77
85
|
SupportsAirPlayAudio: 1n << 9n,
|
|
78
86
|
AudioRedundant: 1n << 11n,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
Authentication4: 1n << 14n,
|
|
88
|
+
MetadataFeatures0: 1n << 15n,
|
|
89
|
+
MetadataFeatures1: 1n << 16n,
|
|
90
|
+
MetadataFeatures2: 1n << 17n,
|
|
91
|
+
AudioFormats0: 1n << 18n,
|
|
92
|
+
AudioFormats1: 1n << 19n,
|
|
93
|
+
AudioFormats2: 1n << 20n,
|
|
94
|
+
AudioFormats3: 1n << 21n,
|
|
95
|
+
Authentication1: 1n << 23n,
|
|
96
|
+
Authentication8: 1n << 26n,
|
|
89
97
|
SupportsLegacyPairing: 1n << 27n,
|
|
90
98
|
HasUnifiedAdvertiserInfo: 1n << 30n,
|
|
91
|
-
|
|
99
|
+
SupportsVolume: 1n << 32n,
|
|
92
100
|
SupportsAirPlayVideoPlayQueue: 1n << 33n,
|
|
93
101
|
SupportsAirPlayFromCloud: 1n << 34n,
|
|
94
|
-
|
|
102
|
+
SupportsTLSPSK: 1n << 35n,
|
|
95
103
|
SupportsUnifiedMediaControl: 1n << 38n,
|
|
96
104
|
SupportsBufferedAudio: 1n << 40n,
|
|
97
105
|
SupportsPTP: 1n << 41n,
|
|
@@ -101,7 +109,7 @@ const AirPlayFeatureFlags = {
|
|
|
101
109
|
SupportsHKPairingAndAccessControl: 1n << 46n,
|
|
102
110
|
SupportsCoreUtilsPairingAndEncryption: 1n << 48n,
|
|
103
111
|
SupportsAirPlayVideoV2: 1n << 49n,
|
|
104
|
-
|
|
112
|
+
MetadataFeatures3: 1n << 50n,
|
|
105
113
|
SupportsUnifiedPairSetupAndMFi: 1n << 51n,
|
|
106
114
|
SupportsSetPeersExtendedMessage: 1n << 52n,
|
|
107
115
|
SupportsAPSync: 1n << 54n,
|
|
@@ -144,7 +152,7 @@ function parseFeatures(features) {
|
|
|
144
152
|
* @returns True if the flag is set.
|
|
145
153
|
*/
|
|
146
154
|
function hasFeatureFlag(features, flag) {
|
|
147
|
-
return (features & flag)
|
|
155
|
+
return (features & flag) === flag;
|
|
148
156
|
}
|
|
149
157
|
/**
|
|
150
158
|
* Returns the names of all feature flags that are set in the given bitmask.
|
|
@@ -216,6 +224,21 @@ function isRemoteControlSupported(txt) {
|
|
|
216
224
|
if (!featuresStr) return false;
|
|
217
225
|
return hasFeatureFlag(parseFeatures(featuresStr), AirPlayFeatureFlags.SupportsHangdogRemoteControl);
|
|
218
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Feature bitmask advertised when connecting for remote control sessions.
|
|
229
|
+
*
|
|
230
|
+
* Includes media control, system pairing, encryption, volume, and
|
|
231
|
+
* hangdog remote control capabilities.
|
|
232
|
+
*/
|
|
233
|
+
const SENDER_FEATURES_REMOTE_CONTROL = AirPlayFeatureFlags.SupportsAirPlayAudio | AirPlayFeatureFlags.AudioRedundant | AirPlayFeatureFlags.MetadataFeatures0 | AirPlayFeatureFlags.MetadataFeatures1 | AirPlayFeatureFlags.MetadataFeatures2 | AirPlayFeatureFlags.MetadataFeatures3 | AirPlayFeatureFlags.Authentication4 | AirPlayFeatureFlags.Authentication1 | AirPlayFeatureFlags.HasUnifiedAdvertiserInfo | AirPlayFeatureFlags.SupportsUnifiedMediaControl | AirPlayFeatureFlags.SupportsSystemPairing | AirPlayFeatureFlags.SupportsCoreUtilsPairingAndEncryption | AirPlayFeatureFlags.SupportsHKPairingAndAccessControl | AirPlayFeatureFlags.SupportsHangdogRemoteControl | AirPlayFeatureFlags.SupportsAPSync | AirPlayFeatureFlags.SupportsSetPeersExtendedMessage | AirPlayFeatureFlags.SupportsVolume;
|
|
234
|
+
/**
|
|
235
|
+
* Feature bitmask advertised when connecting for audio streaming sessions.
|
|
236
|
+
*
|
|
237
|
+
* Extends the remote control features with buffered audio, audio stream
|
|
238
|
+
* connection setup, metadata control, format negotiation, and PTP
|
|
239
|
+
* synchronization support.
|
|
240
|
+
*/
|
|
241
|
+
const SENDER_FEATURES_AUDIO = SENDER_FEATURES_REMOTE_CONTROL | AirPlayFeatureFlags.SupportsBufferedAudio | AirPlayFeatureFlags.SupportsAudioStreamConnectionSetup | AirPlayFeatureFlags.SupportsAudioMetadataControl | AirPlayFeatureFlags.AudioFormats0 | AirPlayFeatureFlags.AudioFormats1 | AirPlayFeatureFlags.AudioFormats2 | AirPlayFeatureFlags.AudioFormats3 | AirPlayFeatureFlags.SupportsPTP;
|
|
219
242
|
|
|
220
243
|
//#endregion
|
|
221
244
|
//#region src/storage.ts
|
|
@@ -418,12 +441,20 @@ async function waitFor(ms) {
|
|
|
418
441
|
|
|
419
442
|
//#endregion
|
|
420
443
|
//#region src/const.ts
|
|
444
|
+
/** Default number of bytes per audio channel sample (16-bit PCM). */
|
|
445
|
+
const AUDIO_BYTES_PER_CHANNEL = 2;
|
|
446
|
+
/** Default number of audio channels (stereo). */
|
|
447
|
+
const AUDIO_CHANNELS = 2;
|
|
448
|
+
/** Number of PCM audio frames packed into a single RTP packet (ALAC/RAOP standard). */
|
|
449
|
+
const AUDIO_FRAMES_PER_PACKET = 352;
|
|
450
|
+
/** Default audio sample rate in Hz (CD quality). */
|
|
451
|
+
const AUDIO_SAMPLE_RATE = 44100;
|
|
421
452
|
/** Default PIN used for transient (non-persistent) AirPlay pairing sessions. */
|
|
422
453
|
const AIRPLAY_TRANSIENT_PIN = "3939";
|
|
423
454
|
/** Timeout in milliseconds for HTTP requests during setup and control. */
|
|
424
455
|
const HTTP_TIMEOUT = 6e3;
|
|
425
|
-
/** Timeout in milliseconds for TCP socket connections during initial connect. */
|
|
426
|
-
const SOCKET_TIMEOUT =
|
|
456
|
+
/** Timeout in milliseconds for TCP socket connections during initial connect. Matches Apple's 30s default. */
|
|
457
|
+
const SOCKET_TIMEOUT = 3e4;
|
|
427
458
|
/** mDNS service type for AirPlay device discovery. */
|
|
428
459
|
const AIRPLAY_SERVICE = "_airplay._tcp.local";
|
|
429
460
|
/** mDNS service type for Companion Link (remote control protocol) device discovery. */
|
|
@@ -788,6 +819,7 @@ const decodeSrvRecord = (buf, offset) => {
|
|
|
788
819
|
*/
|
|
789
820
|
const decodeResource = (buf, offset) => {
|
|
790
821
|
const [qname, nameEnd] = decodeQName(buf, offset);
|
|
822
|
+
if (nameEnd + 10 > buf.byteLength) return null;
|
|
791
823
|
const qtype = buf.readUInt16BE(nameEnd);
|
|
792
824
|
const qclass = buf.readUInt16BE(nameEnd + 2);
|
|
793
825
|
const ttl = buf.readUInt32BE(nameEnd + 4);
|
|
@@ -832,7 +864,7 @@ const decodeResource = (buf, offset) => {
|
|
|
832
864
|
* @param buf - The raw DNS response packet.
|
|
833
865
|
* @returns The parsed header, answer records, and additional resource records.
|
|
834
866
|
*/
|
|
835
|
-
|
|
867
|
+
function decodeDnsResponse(buf) {
|
|
836
868
|
const header = decodeDnsHeader(buf);
|
|
837
869
|
let offset = 12;
|
|
838
870
|
for (let i = 0; i < header.qdcount; i++) {
|
|
@@ -841,17 +873,22 @@ const decodeDnsResponse = (buf) => {
|
|
|
841
873
|
}
|
|
842
874
|
const answers = [];
|
|
843
875
|
for (let i = 0; i < header.ancount; i++) {
|
|
844
|
-
const
|
|
876
|
+
const result = decodeResource(buf, offset);
|
|
877
|
+
if (!result) break;
|
|
878
|
+
const [record, newOffset] = result;
|
|
845
879
|
answers.push(record);
|
|
846
880
|
offset = newOffset;
|
|
847
881
|
}
|
|
848
882
|
for (let i = 0; i < header.nscount; i++) {
|
|
849
|
-
const
|
|
850
|
-
|
|
883
|
+
const result = decodeResource(buf, offset);
|
|
884
|
+
if (!result) break;
|
|
885
|
+
offset = result[1];
|
|
851
886
|
}
|
|
852
887
|
const resources = [];
|
|
853
888
|
for (let i = 0; i < header.arcount; i++) {
|
|
854
|
-
const
|
|
889
|
+
const result = decodeResource(buf, offset);
|
|
890
|
+
if (!result) break;
|
|
891
|
+
const [record, newOffset] = result;
|
|
855
892
|
resources.push(record);
|
|
856
893
|
offset = newOffset;
|
|
857
894
|
}
|
|
@@ -860,7 +897,7 @@ const decodeDnsResponse = (buf) => {
|
|
|
860
897
|
answers,
|
|
861
898
|
resources
|
|
862
899
|
};
|
|
863
|
-
}
|
|
900
|
+
}
|
|
864
901
|
/**
|
|
865
902
|
* Aggregates DNS records from multiple mDNS responses and resolves them
|
|
866
903
|
* into complete service descriptions. Correlates PTR, SRV, TXT, and A records
|
|
@@ -927,7 +964,7 @@ var ServiceCollector = class {
|
|
|
927
964
|
}
|
|
928
965
|
};
|
|
929
966
|
/** Well-known ports used to wake sleeping Apple devices via TCP SYN. */
|
|
930
|
-
const WAKE_PORTS
|
|
967
|
+
const WAKE_PORTS = [
|
|
931
968
|
7e3,
|
|
932
969
|
3689,
|
|
933
970
|
49152,
|
|
@@ -940,8 +977,8 @@ const WAKE_PORTS$1 = [
|
|
|
940
977
|
* @param address - The IP address of the device to wake.
|
|
941
978
|
* @returns A promise that resolves when all knock attempts complete (success or failure).
|
|
942
979
|
*/
|
|
943
|
-
|
|
944
|
-
const promises = WAKE_PORTS
|
|
980
|
+
function knock(address) {
|
|
981
|
+
const promises = WAKE_PORTS.map((port) => new Promise((resolve) => {
|
|
945
982
|
const socket = createConnection({
|
|
946
983
|
host: address,
|
|
947
984
|
port,
|
|
@@ -961,7 +998,7 @@ const knock = (address) => {
|
|
|
961
998
|
});
|
|
962
999
|
}));
|
|
963
1000
|
return Promise.all(promises).then(() => {});
|
|
964
|
-
}
|
|
1001
|
+
}
|
|
965
1002
|
/**
|
|
966
1003
|
* Performs unicast DNS-SD queries to specific hosts. First wakes the devices
|
|
967
1004
|
* via TCP knocking, then repeatedly sends DNS queries via UDP to port 5353 on
|
|
@@ -1274,13 +1311,6 @@ const reporter = new Reporter();
|
|
|
1274
1311
|
//#region src/discovery.ts
|
|
1275
1312
|
/** Cache time-to-live in milliseconds. */
|
|
1276
1313
|
const CACHE_TTL = 3e4;
|
|
1277
|
-
/** Ports used for wake-on-network "knocking" to wake sleeping Apple devices. */
|
|
1278
|
-
const WAKE_PORTS = [
|
|
1279
|
-
7e3,
|
|
1280
|
-
3689,
|
|
1281
|
-
49152,
|
|
1282
|
-
32498
|
|
1283
|
-
];
|
|
1284
1314
|
/**
|
|
1285
1315
|
* Converts a raw mDNS service record into a {@link DiscoveryResult}.
|
|
1286
1316
|
*
|
|
@@ -1393,27 +1423,8 @@ var Discovery = class Discovery {
|
|
|
1393
1423
|
*
|
|
1394
1424
|
* @param address - The IP address of the device to wake.
|
|
1395
1425
|
*/
|
|
1396
|
-
static
|
|
1397
|
-
|
|
1398
|
-
const socket = createConnection({
|
|
1399
|
-
host: address,
|
|
1400
|
-
port,
|
|
1401
|
-
timeout: 500
|
|
1402
|
-
});
|
|
1403
|
-
socket.on("connect", () => {
|
|
1404
|
-
socket.destroy();
|
|
1405
|
-
resolve();
|
|
1406
|
-
});
|
|
1407
|
-
socket.on("error", () => {
|
|
1408
|
-
socket.destroy();
|
|
1409
|
-
resolve();
|
|
1410
|
-
});
|
|
1411
|
-
socket.on("timeout", () => {
|
|
1412
|
-
socket.destroy();
|
|
1413
|
-
resolve();
|
|
1414
|
-
});
|
|
1415
|
-
}));
|
|
1416
|
-
await Promise.all(promises);
|
|
1426
|
+
static wake(address) {
|
|
1427
|
+
return knock(address);
|
|
1417
1428
|
}
|
|
1418
1429
|
/**
|
|
1419
1430
|
* Discovers all Apple devices on the network across all supported service types
|
|
@@ -1892,6 +1903,42 @@ var Context = class {
|
|
|
1892
1903
|
}
|
|
1893
1904
|
};
|
|
1894
1905
|
|
|
1906
|
+
//#endregion
|
|
1907
|
+
//#region src/hkdf.ts
|
|
1908
|
+
/**
|
|
1909
|
+
* Derives a pair of ChaCha20 encryption keys (read + write) from a shared secret
|
|
1910
|
+
* using HKDF-SHA512 with direction-specific info strings.
|
|
1911
|
+
*
|
|
1912
|
+
* This is a shared helper used across AirPlay, Companion Link, and RAOP pairing
|
|
1913
|
+
* flows to eliminate repeated HKDF boilerplate. The salt and info strings vary
|
|
1914
|
+
* per protocol and stream type.
|
|
1915
|
+
*
|
|
1916
|
+
* @param sharedSecret - The shared secret from a pair-verify or pair-setup flow.
|
|
1917
|
+
* @param salt - HKDF salt string (protocol-specific, e.g. 'Control-Salt').
|
|
1918
|
+
* @param readInfo - HKDF info string for the read (decrypt) key.
|
|
1919
|
+
* @param writeInfo - HKDF info string for the write (encrypt) key.
|
|
1920
|
+
* @returns An object with `readKey` and `writeKey` as 32-byte Buffers.
|
|
1921
|
+
*/
|
|
1922
|
+
function deriveEncryptionKeys(sharedSecret, salt, readInfo, writeInfo) {
|
|
1923
|
+
const saltBuffer = Buffer.from(salt);
|
|
1924
|
+
return {
|
|
1925
|
+
readKey: hkdf({
|
|
1926
|
+
hash: "sha512",
|
|
1927
|
+
key: sharedSecret,
|
|
1928
|
+
length: 32,
|
|
1929
|
+
salt: saltBuffer,
|
|
1930
|
+
info: Buffer.from(readInfo)
|
|
1931
|
+
}),
|
|
1932
|
+
writeKey: hkdf({
|
|
1933
|
+
hash: "sha512",
|
|
1934
|
+
key: sharedSecret,
|
|
1935
|
+
length: 32,
|
|
1936
|
+
salt: saltBuffer,
|
|
1937
|
+
info: Buffer.from(writeInfo)
|
|
1938
|
+
})
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1895
1942
|
//#endregion
|
|
1896
1943
|
//#region ../../node_modules/.bun/fast-srp-hap@2.0.4/node_modules/fast-srp-hap/jsbn/jsbn.js
|
|
1897
1944
|
var require_jsbn = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -3965,6 +4012,8 @@ var AccessoryPair = class extends BasePairing {
|
|
|
3965
4012
|
#pairingId;
|
|
3966
4013
|
/** Protocol-specific callback for sending TLV8-encoded pair-setup requests. */
|
|
3967
4014
|
#requestHandler;
|
|
4015
|
+
/** Whether to use AES-128-CTR instead of ChaCha20-Poly1305 for M5/M6 encryption. */
|
|
4016
|
+
#useAes;
|
|
3968
4017
|
/** Ed25519 public key for long-term identity. */
|
|
3969
4018
|
#publicKey;
|
|
3970
4019
|
/** Ed25519 secret key for signing identity proofs. */
|
|
@@ -3975,11 +4024,17 @@ var AccessoryPair = class extends BasePairing {
|
|
|
3975
4024
|
* @param context - Shared context for logging and device identity.
|
|
3976
4025
|
* @param requestHandler - Callback that sends TLV8-encoded data and returns the response.
|
|
3977
4026
|
*/
|
|
3978
|
-
|
|
4027
|
+
/**
|
|
4028
|
+
* @param context - Shared context for logging and device identity.
|
|
4029
|
+
* @param requestHandler - Callback that sends TLV8-encoded data and returns the response.
|
|
4030
|
+
* @param useAes - Use AES-128-CTR instead of ChaCha20-Poly1305 for M5/M6 encryption (legacy devices).
|
|
4031
|
+
*/
|
|
4032
|
+
constructor(context, requestHandler, useAes = false) {
|
|
3979
4033
|
super(context);
|
|
3980
4034
|
this.#name = "basmilius/apple-protocols";
|
|
3981
4035
|
this.#pairingId = Buffer.from(v4().toUpperCase());
|
|
3982
4036
|
this.#requestHandler = requestHandler;
|
|
4037
|
+
this.#useAes = useAes;
|
|
3983
4038
|
}
|
|
3984
4039
|
/** Generates a new Ed25519 key pair for long-term identity. Must be called before pin() or transient(). */
|
|
3985
4040
|
async start() {
|
|
@@ -4020,20 +4075,7 @@ var AccessoryPair = class extends BasePairing {
|
|
|
4020
4075
|
const m2 = await this.m2(m1);
|
|
4021
4076
|
const m3 = await this.m3(m2);
|
|
4022
4077
|
const m4 = await this.m4(m3);
|
|
4023
|
-
const accessoryToControllerKey =
|
|
4024
|
-
hash: "sha512",
|
|
4025
|
-
key: m4.sharedSecret,
|
|
4026
|
-
length: 32,
|
|
4027
|
-
salt: Buffer.from("Control-Salt"),
|
|
4028
|
-
info: Buffer.from("Control-Read-Encryption-Key")
|
|
4029
|
-
});
|
|
4030
|
-
const controllerToAccessoryKey = hkdf({
|
|
4031
|
-
hash: "sha512",
|
|
4032
|
-
key: m4.sharedSecret,
|
|
4033
|
-
length: 32,
|
|
4034
|
-
salt: Buffer.from("Control-Salt"),
|
|
4035
|
-
info: Buffer.from("Control-Write-Encryption-Key")
|
|
4036
|
-
});
|
|
4078
|
+
const { readKey: accessoryToControllerKey, writeKey: controllerToAccessoryKey } = deriveEncryptionKeys(m4.sharedSecret, "Control-Salt", "Control-Read-Encryption-Key", "Control-Write-Encryption-Key");
|
|
4037
4079
|
return {
|
|
4038
4080
|
pairingId: this.#pairingId,
|
|
4039
4081
|
sharedSecret: m4.sharedSecret,
|
|
@@ -4135,10 +4177,34 @@ var AccessoryPair = class extends BasePairing {
|
|
|
4135
4177
|
[TLV8.Value.Signature, Buffer.from(signature)],
|
|
4136
4178
|
[TLV8.Value.Name, OPack.encode({ name: this.#name })]
|
|
4137
4179
|
]);
|
|
4138
|
-
|
|
4139
|
-
|
|
4180
|
+
let encrypted;
|
|
4181
|
+
if (this.#useAes) {
|
|
4182
|
+
const aesKey = hkdf({
|
|
4183
|
+
hash: "sha512",
|
|
4184
|
+
key: m4.sharedSecret,
|
|
4185
|
+
length: 16,
|
|
4186
|
+
salt: Buffer.alloc(0),
|
|
4187
|
+
info: Buffer.from("Pair-Setup-AES-Key")
|
|
4188
|
+
});
|
|
4189
|
+
const aesIv = hkdf({
|
|
4190
|
+
hash: "sha512",
|
|
4191
|
+
key: m4.sharedSecret,
|
|
4192
|
+
length: 16,
|
|
4193
|
+
salt: Buffer.alloc(0),
|
|
4194
|
+
info: Buffer.from("Pair-Setup-AES-IV")
|
|
4195
|
+
});
|
|
4196
|
+
encrypted = Aes.encrypt(aesKey, aesIv, innerTlv);
|
|
4197
|
+
} else {
|
|
4198
|
+
const { authTag, ciphertext } = Chacha20.encrypt(sessionKey, Buffer.from("PS-Msg05"), null, innerTlv);
|
|
4199
|
+
encrypted = Buffer.concat([ciphertext, authTag]);
|
|
4200
|
+
}
|
|
4140
4201
|
const response = await this.#requestHandler("m5", TLV8.encode([[TLV8.Value.State, TLV8.State.M5], [TLV8.Value.EncryptedData, encrypted]]));
|
|
4141
4202
|
const encryptedDataRaw = this.tlv(response).get(TLV8.Value.EncryptedData);
|
|
4203
|
+
if (this.#useAes) return {
|
|
4204
|
+
authTag: Buffer.alloc(0),
|
|
4205
|
+
data: encryptedDataRaw,
|
|
4206
|
+
sessionKey
|
|
4207
|
+
};
|
|
4142
4208
|
const encryptedData = encryptedDataRaw.subarray(0, -16);
|
|
4143
4209
|
return {
|
|
4144
4210
|
authTag: encryptedDataRaw.subarray(-16),
|
|
@@ -4156,7 +4222,24 @@ var AccessoryPair = class extends BasePairing {
|
|
|
4156
4222
|
* Returns the accessory's long-term public key and identifier for future Pair-Verify sessions.
|
|
4157
4223
|
*/
|
|
4158
4224
|
async m6(m4, m5) {
|
|
4159
|
-
|
|
4225
|
+
let data;
|
|
4226
|
+
if (this.#useAes) {
|
|
4227
|
+
const aesKey = hkdf({
|
|
4228
|
+
hash: "sha512",
|
|
4229
|
+
key: m4.sharedSecret,
|
|
4230
|
+
length: 16,
|
|
4231
|
+
salt: Buffer.alloc(0),
|
|
4232
|
+
info: Buffer.from("Pair-Setup-AES-Key")
|
|
4233
|
+
});
|
|
4234
|
+
const aesIv = hkdf({
|
|
4235
|
+
hash: "sha512",
|
|
4236
|
+
key: m4.sharedSecret,
|
|
4237
|
+
length: 16,
|
|
4238
|
+
salt: Buffer.alloc(0),
|
|
4239
|
+
info: Buffer.from("Pair-Setup-AES-IV")
|
|
4240
|
+
});
|
|
4241
|
+
data = Aes.decrypt(aesKey, aesIv, m5.data);
|
|
4242
|
+
} else data = Chacha20.decrypt(m5.sessionKey, Buffer.from("PS-Msg06"), null, m5.data, m5.authTag);
|
|
4160
4243
|
const tlv = TLV8.decode(data);
|
|
4161
4244
|
const accessoryIdentifier = tlv.get(TLV8.Value.Identifier);
|
|
4162
4245
|
const accessoryLongTermPublicKey = tlv.get(TLV8.Value.PublicKey);
|
|
@@ -4197,14 +4280,18 @@ var AccessoryVerify = class extends BasePairing {
|
|
|
4197
4280
|
#ephemeralKeyPair;
|
|
4198
4281
|
/** Protocol-specific callback for sending TLV8-encoded pair-verify requests. */
|
|
4199
4282
|
#requestHandler;
|
|
4283
|
+
/** Whether to use AES-128-CTR instead of ChaCha20-Poly1305 for encryption. */
|
|
4284
|
+
#useAes;
|
|
4200
4285
|
/**
|
|
4201
4286
|
* @param context - Shared context for logging and device identity.
|
|
4202
4287
|
* @param requestHandler - Callback that sends TLV8-encoded data and returns the response.
|
|
4288
|
+
* @param useAes - Use AES-128-CTR instead of ChaCha20-Poly1305 for encryption (legacy devices).
|
|
4203
4289
|
*/
|
|
4204
|
-
constructor(context, requestHandler) {
|
|
4290
|
+
constructor(context, requestHandler, useAes = false) {
|
|
4205
4291
|
super(context);
|
|
4206
4292
|
this.#ephemeralKeyPair = Curve25519.generateKeyPair();
|
|
4207
4293
|
this.#requestHandler = requestHandler;
|
|
4294
|
+
this.#useAes = useAes;
|
|
4208
4295
|
}
|
|
4209
4296
|
/**
|
|
4210
4297
|
* Performs the full pair-verify flow (M1 through M4) using stored credentials
|
|
@@ -4255,9 +4342,28 @@ var AccessoryVerify = class extends BasePairing {
|
|
|
4255
4342
|
salt: Buffer.from("Pair-Verify-Encrypt-Salt"),
|
|
4256
4343
|
info: Buffer.from("Pair-Verify-Encrypt-Info")
|
|
4257
4344
|
});
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4345
|
+
let data;
|
|
4346
|
+
if (this.#useAes) {
|
|
4347
|
+
const aesKey = hkdf({
|
|
4348
|
+
hash: "sha512",
|
|
4349
|
+
key: sharedSecret,
|
|
4350
|
+
length: 16,
|
|
4351
|
+
salt: Buffer.alloc(0),
|
|
4352
|
+
info: Buffer.from("Pair-Verify-AES-Key")
|
|
4353
|
+
});
|
|
4354
|
+
const aesIv = hkdf({
|
|
4355
|
+
hash: "sha512",
|
|
4356
|
+
key: sharedSecret,
|
|
4357
|
+
length: 16,
|
|
4358
|
+
salt: Buffer.alloc(0),
|
|
4359
|
+
info: Buffer.from("Pair-Verify-AES-IV")
|
|
4360
|
+
});
|
|
4361
|
+
data = Aes.decrypt(aesKey, aesIv, m1.encryptedData);
|
|
4362
|
+
} else {
|
|
4363
|
+
const encryptedData = m1.encryptedData.subarray(0, -16);
|
|
4364
|
+
const encryptedTag = m1.encryptedData.subarray(-16);
|
|
4365
|
+
data = Chacha20.decrypt(sessionKey, Buffer.from("PV-Msg02"), null, encryptedData, encryptedTag);
|
|
4366
|
+
}
|
|
4261
4367
|
const tlv = TLV8.decode(data);
|
|
4262
4368
|
const accessoryIdentifier = tlv.get(TLV8.Value.Identifier);
|
|
4263
4369
|
const accessorySignature = tlv.get(TLV8.Value.Signature);
|
|
@@ -4289,8 +4395,27 @@ var AccessoryVerify = class extends BasePairing {
|
|
|
4289
4395
|
]);
|
|
4290
4396
|
const iosDeviceSignature = Buffer.from(Ed25519.sign(iosDeviceInfo, secretKey));
|
|
4291
4397
|
const innerTlv = TLV8.encode([[TLV8.Value.Identifier, pairingId], [TLV8.Value.Signature, iosDeviceSignature]]);
|
|
4292
|
-
|
|
4293
|
-
|
|
4398
|
+
let encrypted;
|
|
4399
|
+
if (this.#useAes) {
|
|
4400
|
+
const aesKey = hkdf({
|
|
4401
|
+
hash: "sha512",
|
|
4402
|
+
key: m2.sharedSecret,
|
|
4403
|
+
length: 16,
|
|
4404
|
+
salt: Buffer.alloc(0),
|
|
4405
|
+
info: Buffer.from("Pair-Verify-AES-Key")
|
|
4406
|
+
});
|
|
4407
|
+
const aesIv = hkdf({
|
|
4408
|
+
hash: "sha512",
|
|
4409
|
+
key: m2.sharedSecret,
|
|
4410
|
+
length: 16,
|
|
4411
|
+
salt: Buffer.alloc(0),
|
|
4412
|
+
info: Buffer.from("Pair-Verify-AES-IV")
|
|
4413
|
+
});
|
|
4414
|
+
encrypted = Aes.encrypt(aesKey, aesIv, innerTlv);
|
|
4415
|
+
} else {
|
|
4416
|
+
const { authTag, ciphertext } = Chacha20.encrypt(m2.sessionKey, Buffer.from("PV-Msg03"), null, innerTlv);
|
|
4417
|
+
encrypted = Buffer.concat([ciphertext, authTag]);
|
|
4418
|
+
}
|
|
4294
4419
|
await this.#requestHandler("m3", TLV8.encode([[TLV8.Value.State, TLV8.State.M3], [TLV8.Value.EncryptedData, encrypted]]));
|
|
4295
4420
|
return {};
|
|
4296
4421
|
}
|
|
@@ -4488,16 +4613,16 @@ var TimingServer = class {
|
|
|
4488
4613
|
constructor() {
|
|
4489
4614
|
this.#logger = new Logger("timing-server");
|
|
4490
4615
|
this.#socket = createSocket("udp4");
|
|
4491
|
-
this.onConnect = this.onConnect.bind(this);
|
|
4492
4616
|
this.onError = this.onError.bind(this);
|
|
4493
4617
|
this.onMessage = this.onMessage.bind(this);
|
|
4494
|
-
this.#socket.on("connect", this.onConnect);
|
|
4495
4618
|
this.#socket.on("error", this.onError);
|
|
4496
4619
|
this.#socket.on("message", this.onMessage);
|
|
4497
4620
|
}
|
|
4498
4621
|
/** Closes the UDP socket and resets the port. */
|
|
4499
4622
|
close() {
|
|
4500
|
-
|
|
4623
|
+
try {
|
|
4624
|
+
this.#socket.close();
|
|
4625
|
+
} catch {}
|
|
4501
4626
|
this.#port = 0;
|
|
4502
4627
|
}
|
|
4503
4628
|
/**
|
|
@@ -4523,13 +4648,6 @@ var TimingServer = class {
|
|
|
4523
4648
|
});
|
|
4524
4649
|
}
|
|
4525
4650
|
/**
|
|
4526
|
-
* Handles the socket 'connect' event by configuring buffer sizes.
|
|
4527
|
-
*/
|
|
4528
|
-
onConnect() {
|
|
4529
|
-
this.#socket.setRecvBufferSize(16384);
|
|
4530
|
-
this.#socket.setSendBufferSize(16384);
|
|
4531
|
-
}
|
|
4532
|
-
/**
|
|
4533
4651
|
* Handles socket errors by logging them.
|
|
4534
4652
|
*
|
|
4535
4653
|
* @param err - The error that occurred.
|
|
@@ -4589,7 +4707,7 @@ var TimingServer = class {
|
|
|
4589
4707
|
* @returns A random 32-bit unsigned integer as a decimal string.
|
|
4590
4708
|
*/
|
|
4591
4709
|
function generateActiveRemoteId() {
|
|
4592
|
-
return
|
|
4710
|
+
return randomBytes(4).readUInt32BE(0).toString(10);
|
|
4593
4711
|
}
|
|
4594
4712
|
/**
|
|
4595
4713
|
* Generates a random DACP-ID for identifying this controller in DACP sessions.
|
|
@@ -4597,7 +4715,7 @@ function generateActiveRemoteId() {
|
|
|
4597
4715
|
* @returns A random 64-bit integer as an uppercase hexadecimal string.
|
|
4598
4716
|
*/
|
|
4599
4717
|
function generateDacpId() {
|
|
4600
|
-
return
|
|
4718
|
+
return randomBytes(8).toString("hex").toUpperCase();
|
|
4601
4719
|
}
|
|
4602
4720
|
/**
|
|
4603
4721
|
* Generates a random session identifier for RTSP and AirPlay sessions.
|
|
@@ -4605,7 +4723,7 @@ function generateDacpId() {
|
|
|
4605
4723
|
* @returns A random 32-bit unsigned integer as a decimal string.
|
|
4606
4724
|
*/
|
|
4607
4725
|
function generateSessionId() {
|
|
4608
|
-
return
|
|
4726
|
+
return randomBytes(4).readUInt32BE(0).toString(10);
|
|
4609
4727
|
}
|
|
4610
4728
|
/**
|
|
4611
4729
|
* Finds the first non-internal IPv4 address on this machine.
|
|
@@ -4789,42 +4907,54 @@ const MODEL_TYPES = {
|
|
|
4789
4907
|
* @param identifier - Model identifier (e.g. "AppleTV6,2") or internal name (e.g. "J305AP").
|
|
4790
4908
|
* @returns The matching device model, or {@link DeviceModel.Unknown} if unrecognized.
|
|
4791
4909
|
*/
|
|
4792
|
-
|
|
4910
|
+
function lookupDeviceModel(identifier) {
|
|
4911
|
+
return MODEL_IDENTIFIERS[identifier] ?? INTERNAL_NAMES[identifier] ?? DeviceModel.Unknown;
|
|
4912
|
+
}
|
|
4793
4913
|
/**
|
|
4794
4914
|
* Returns the human-readable display name for a device model.
|
|
4795
4915
|
*
|
|
4796
4916
|
* @param model - The device model to look up.
|
|
4797
4917
|
* @returns A display name like "Apple TV 4K (2nd generation)", or "Unknown".
|
|
4798
4918
|
*/
|
|
4799
|
-
|
|
4919
|
+
function getDeviceModelName(model) {
|
|
4920
|
+
return MODEL_NAMES[model] ?? "Unknown";
|
|
4921
|
+
}
|
|
4800
4922
|
/**
|
|
4801
4923
|
* Returns the high-level device type category for a device model.
|
|
4802
4924
|
*
|
|
4803
4925
|
* @param model - The device model to categorize.
|
|
4804
4926
|
* @returns The device type (AppleTV, HomePod, AirPort, or Unknown).
|
|
4805
4927
|
*/
|
|
4806
|
-
|
|
4928
|
+
function getDeviceType(model) {
|
|
4929
|
+
return MODEL_TYPES[model] ?? DeviceType.Unknown;
|
|
4930
|
+
}
|
|
4807
4931
|
/**
|
|
4808
4932
|
* Checks whether the given model is an Apple TV.
|
|
4809
4933
|
*
|
|
4810
4934
|
* @param model - The device model to check.
|
|
4811
4935
|
* @returns True if the model is any Apple TV generation.
|
|
4812
4936
|
*/
|
|
4813
|
-
|
|
4937
|
+
function isAppleTV(model) {
|
|
4938
|
+
return getDeviceType(model) === DeviceType.AppleTV;
|
|
4939
|
+
}
|
|
4814
4940
|
/**
|
|
4815
4941
|
* Checks whether the given model is a HomePod.
|
|
4816
4942
|
*
|
|
4817
4943
|
* @param model - The device model to check.
|
|
4818
4944
|
* @returns True if the model is any HomePod variant.
|
|
4819
4945
|
*/
|
|
4820
|
-
|
|
4946
|
+
function isHomePod(model) {
|
|
4947
|
+
return getDeviceType(model) === DeviceType.HomePod;
|
|
4948
|
+
}
|
|
4821
4949
|
/**
|
|
4822
4950
|
* Checks whether the given model is an AirPort Express.
|
|
4823
4951
|
*
|
|
4824
4952
|
* @param model - The device model to check.
|
|
4825
4953
|
* @returns True if the model is any AirPort Express generation.
|
|
4826
4954
|
*/
|
|
4827
|
-
|
|
4955
|
+
function isAirPort(model) {
|
|
4956
|
+
return getDeviceType(model) === DeviceType.AirPort;
|
|
4957
|
+
}
|
|
4828
4958
|
|
|
4829
4959
|
//#endregion
|
|
4830
|
-
export { AIRPLAY_SERVICE, AIRPLAY_TRANSIENT_PIN, AccessoryPair, AccessoryVerify, AirPlayFeatureFlags, AppleProtocolError, AuthenticationError, COMPANION_LINK_SERVICE, CommandError, Connection, ConnectionClosedError, ConnectionError, ConnectionRecovery, ConnectionTimeoutError, Context, CredentialsError, DeviceModel, DeviceType, Discovery, DiscoveryError, ENCRYPTION, EncryptionAwareConnection, EncryptionError, EncryptionState, HTTP_TIMEOUT, InvalidResponseError, JsonStorage, MemoryStorage, PairingError, PlaybackError, RAOP_SERVICE, SetupError, Storage, TimeoutError, TimingServer, describeFlags, generateActiveRemoteId, generateDacpId, generateSessionId, getDeviceModelName, getDeviceType, getLocalIP, getMacAddress, getPairingRequirement, getProtocolVersion, hasFeatureFlag, isAirPort, isAppleTV, isHomePod, isPasswordRequired, isRemoteControlSupported, lookupDeviceModel, multicast as mdnsMulticast, unicast as mdnsUnicast, parseFeatures, prompt, randomInt32, randomInt64, reporter, uint16ToBE, uint53ToLE, v4 as uuid, waitFor };
|
|
4960
|
+
export { AIRPLAY_SERVICE, AIRPLAY_TRANSIENT_PIN, AUDIO_BYTES_PER_CHANNEL, AUDIO_CHANNELS, AUDIO_FRAMES_PER_PACKET, AUDIO_SAMPLE_RATE, AccessoryPair, AccessoryVerify, AirPlayFeatureFlags, AppleProtocolError, AuthenticationError, COMPANION_LINK_SERVICE, CommandError, Connection, ConnectionClosedError, ConnectionError, ConnectionRecovery, ConnectionTimeoutError, Context, CredentialsError, DeviceModel, DeviceType, Discovery, DiscoveryError, ENCRYPTION, EncryptionAwareConnection, EncryptionError, EncryptionState, HTTP_TIMEOUT, InvalidResponseError, JsonStorage, MemoryStorage, PairingError, PlaybackError, RAOP_SERVICE, SENDER_FEATURES_AUDIO, SENDER_FEATURES_REMOTE_CONTROL, SOCKET_TIMEOUT, SetupError, Storage, TimeoutError, TimingServer, deriveEncryptionKeys, describeFlags, generateActiveRemoteId, generateDacpId, generateSessionId, getDeviceModelName, getDeviceType, getLocalIP, getMacAddress, getPairingRequirement, getProtocolVersion, hasFeatureFlag, isAirPort, isAppleTV, isHomePod, isPasswordRequired, isRemoteControlSupported, lookupDeviceModel, multicast as mdnsMulticast, unicast as mdnsUnicast, parseFeatures, prompt, randomInt32, randomInt64, reporter, uint16ToBE, uint53ToLE, v4 as uuid, waitFor };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basmilius/apple-common",
|
|
3
3
|
"description": "Common features shared across various apple protocol packages.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.12.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": {
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@basmilius/apple-encoding": "0.
|
|
49
|
-
"@basmilius/apple-encryption": "0.
|
|
48
|
+
"@basmilius/apple-encoding": "0.12.0",
|
|
49
|
+
"@basmilius/apple-encryption": "0.12.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/bun": "^1.3.11",
|
|
53
53
|
"@types/node": "^25.5.0",
|
|
54
54
|
"fast-srp-hap": "^2.0.4",
|
|
55
|
-
"tsdown": "^0.21.
|
|
55
|
+
"tsdown": "^0.21.6",
|
|
56
56
|
"uuid": "^13.0.0"
|
|
57
57
|
}
|
|
58
58
|
}
|