@basmilius/apple-common 0.9.16 → 0.9.17
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 +64 -0
- package/dist/index.mjs +97 -0
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -86,6 +86,7 @@ declare class Logger {
|
|
|
86
86
|
declare class Reporter {
|
|
87
87
|
#private;
|
|
88
88
|
all(): void;
|
|
89
|
+
none(): void;
|
|
89
90
|
disable(group: DebugGroup): void;
|
|
90
91
|
enable(group: DebugGroup): void;
|
|
91
92
|
isEnabled(group: DebugGroup): boolean;
|
|
@@ -107,19 +108,82 @@ declare abstract class BasePairing {
|
|
|
107
108
|
constructor(context: Context);
|
|
108
109
|
tlv(buffer: Buffer): Map<number, Buffer>;
|
|
109
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Implements the HAP (HomeKit Accessory Protocol) Pair-Setup flow using SRP-6a.
|
|
113
|
+
*
|
|
114
|
+
* The pairing process uses Secure Remote Password (SRP) for PIN-based authentication,
|
|
115
|
+
* followed by Ed25519 signature exchange for long-term identity establishment.
|
|
116
|
+
*
|
|
117
|
+
* Flow: M1 (salt exchange) → M2 (SRP proof) → M3 (server proof) → M4 (shared secret)
|
|
118
|
+
* → M5 (encrypted credential exchange) → M6 (accessory verification)
|
|
119
|
+
*
|
|
120
|
+
* For transient pairing (HomePod), only M1-M4 are needed — no long-term credentials are stored.
|
|
121
|
+
*/
|
|
110
122
|
declare class AccessoryPair extends BasePairing {
|
|
111
123
|
#private;
|
|
112
124
|
constructor(context: Context, requestHandler: RequestHandler);
|
|
113
125
|
start(): Promise<void>;
|
|
114
126
|
pin(askPin: () => Promise<string>): Promise<AccessoryCredentials>;
|
|
115
127
|
transient(): Promise<AccessoryKeys>;
|
|
128
|
+
/**
|
|
129
|
+
* SRP Step 1: Initiates pair-setup by requesting the accessory's SRP public key and salt.
|
|
130
|
+
*
|
|
131
|
+
* Sends: TLV with Method=PairSetup, State=M1, optional Flags (e.g. TransientPairing).
|
|
132
|
+
* Receives: Accessory's SRP public key (B) and salt for key derivation.
|
|
133
|
+
*/
|
|
116
134
|
m1(additionalTlv?: [number, number | Buffer][]): Promise<PairM1>;
|
|
135
|
+
/**
|
|
136
|
+
* SRP Step 2: Generates client proof using the PIN.
|
|
137
|
+
*
|
|
138
|
+
* Creates an SRP client with the accessory's salt and public key, then computes
|
|
139
|
+
* the client's public key (A) and proof (M1) which proves knowledge of the PIN.
|
|
140
|
+
*/
|
|
117
141
|
m2(m1: PairM1, pin?: string): Promise<PairM2>;
|
|
142
|
+
/**
|
|
143
|
+
* SRP Step 3: Sends the client's public key and proof; receives the server's proof.
|
|
144
|
+
*
|
|
145
|
+
* Sends: TLV with State=M3, client SRP public key (A), and client proof (M1).
|
|
146
|
+
* Receives: Server's M2-proof, confirming the accessory also knows the PIN.
|
|
147
|
+
*/
|
|
118
148
|
m3(m2: PairM2): Promise<PairM3>;
|
|
149
|
+
/**
|
|
150
|
+
* SRP Step 4: Verifies the server's proof and derives the shared secret.
|
|
151
|
+
*
|
|
152
|
+
* Validates the server's M2-proof, then computes the SRP session key (K).
|
|
153
|
+
* This shared secret is the root key for all subsequent HKDF derivations.
|
|
154
|
+
*/
|
|
119
155
|
m4(m3: PairM3): Promise<PairM4>;
|
|
156
|
+
/**
|
|
157
|
+
* HAP Step 5: Encrypted credential exchange — controller sends its identity.
|
|
158
|
+
*
|
|
159
|
+
* Derives a signing key (HKDF with "Pair-Setup-Controller-Sign-Salt/Info") and
|
|
160
|
+
* a session key (HKDF with "Pair-Setup-Encrypt-Salt/Info") from the SRP shared secret.
|
|
161
|
+
* Signs [signingKey || pairingId || Ed25519PublicKey] and sends it encrypted
|
|
162
|
+
* via ChaCha20-Poly1305 (nonce "PS-Msg05").
|
|
163
|
+
*
|
|
164
|
+
* Receives: Encrypted accessory credentials (decrypted in M6).
|
|
165
|
+
*/
|
|
120
166
|
m5(m4: PairM4): Promise<PairM5>;
|
|
167
|
+
/**
|
|
168
|
+
* HAP Step 6: Verifies the accessory's identity and extracts long-term credentials.
|
|
169
|
+
*
|
|
170
|
+
* Decrypts the accessory's response from M5 via ChaCha20-Poly1305 (nonce "PS-Msg06").
|
|
171
|
+
* Derives "Accessory X" (HKDF with "Pair-Setup-Accessory-Sign-Salt/Info"), then
|
|
172
|
+
* verifies the Ed25519 signature over [accessoryX || accessoryId || accessoryPublicKey].
|
|
173
|
+
*
|
|
174
|
+
* Returns the accessory's long-term public key and identifier for future Pair-Verify sessions.
|
|
175
|
+
*/
|
|
121
176
|
m6(m4: PairM4, m5: PairM5): Promise<AccessoryCredentials>;
|
|
122
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Implements the HAP (HomeKit Accessory Protocol) Pair-Verify flow using Curve25519 ECDH.
|
|
180
|
+
*
|
|
181
|
+
* Used to re-authenticate a previously paired accessory without needing the PIN again.
|
|
182
|
+
* Establishes a new session with forward secrecy via ephemeral Curve25519 key exchange.
|
|
183
|
+
*
|
|
184
|
+
* Flow: M1 (ephemeral key exchange) → M2 (signature verification)
|
|
185
|
+
* → M3 (controller proof) → M4 (session key derivation)
|
|
186
|
+
*/
|
|
123
187
|
declare class AccessoryVerify extends BasePairing {
|
|
124
188
|
#private;
|
|
125
189
|
constructor(context: Context, requestHandler: RequestHandler);
|
package/dist/index.mjs
CHANGED
|
@@ -693,6 +693,8 @@ var Discovery = class Discovery {
|
|
|
693
693
|
const cached = Discovery.#cache.get(this.#service);
|
|
694
694
|
if (cached && cached.expiresAt > Date.now()) return cached.results;
|
|
695
695
|
}
|
|
696
|
+
const now = Date.now();
|
|
697
|
+
for (const [key, entry] of Discovery.#cache) if (entry.expiresAt <= now) Discovery.#cache.delete(key);
|
|
696
698
|
const mapped = (await multicast([this.#service], 4)).map(toDiscoveryResult);
|
|
697
699
|
Discovery.#cache.set(this.#service, {
|
|
698
700
|
results: mapped,
|
|
@@ -1049,6 +1051,9 @@ var Reporter = class {
|
|
|
1049
1051
|
"warn"
|
|
1050
1052
|
];
|
|
1051
1053
|
}
|
|
1054
|
+
none() {
|
|
1055
|
+
this.#enabled = [];
|
|
1056
|
+
}
|
|
1052
1057
|
disable(group) {
|
|
1053
1058
|
if (this.#enabled.includes(group)) this.#enabled.splice(this.#enabled.indexOf(group), 1);
|
|
1054
1059
|
}
|
|
@@ -3135,6 +3140,17 @@ var BasePairing = class {
|
|
|
3135
3140
|
return data;
|
|
3136
3141
|
}
|
|
3137
3142
|
};
|
|
3143
|
+
/**
|
|
3144
|
+
* Implements the HAP (HomeKit Accessory Protocol) Pair-Setup flow using SRP-6a.
|
|
3145
|
+
*
|
|
3146
|
+
* The pairing process uses Secure Remote Password (SRP) for PIN-based authentication,
|
|
3147
|
+
* followed by Ed25519 signature exchange for long-term identity establishment.
|
|
3148
|
+
*
|
|
3149
|
+
* Flow: M1 (salt exchange) → M2 (SRP proof) → M3 (server proof) → M4 (shared secret)
|
|
3150
|
+
* → M5 (encrypted credential exchange) → M6 (accessory verification)
|
|
3151
|
+
*
|
|
3152
|
+
* For transient pairing (HomePod), only M1-M4 are needed — no long-term credentials are stored.
|
|
3153
|
+
*/
|
|
3138
3154
|
var AccessoryPair = class extends BasePairing {
|
|
3139
3155
|
#name;
|
|
3140
3156
|
#pairingId;
|
|
@@ -3189,6 +3205,12 @@ var AccessoryPair = class extends BasePairing {
|
|
|
3189
3205
|
controllerToAccessoryKey
|
|
3190
3206
|
};
|
|
3191
3207
|
}
|
|
3208
|
+
/**
|
|
3209
|
+
* SRP Step 1: Initiates pair-setup by requesting the accessory's SRP public key and salt.
|
|
3210
|
+
*
|
|
3211
|
+
* Sends: TLV with Method=PairSetup, State=M1, optional Flags (e.g. TransientPairing).
|
|
3212
|
+
* Receives: Accessory's SRP public key (B) and salt for key derivation.
|
|
3213
|
+
*/
|
|
3192
3214
|
async m1(additionalTlv = []) {
|
|
3193
3215
|
const response = await this.#requestHandler("m1", TLV8.encode([
|
|
3194
3216
|
[TLV8.Value.Method, TLV8.Method.PairSetup],
|
|
@@ -3201,6 +3223,12 @@ var AccessoryPair = class extends BasePairing {
|
|
|
3201
3223
|
salt: data.get(TLV8.Value.Salt)
|
|
3202
3224
|
};
|
|
3203
3225
|
}
|
|
3226
|
+
/**
|
|
3227
|
+
* SRP Step 2: Generates client proof using the PIN.
|
|
3228
|
+
*
|
|
3229
|
+
* Creates an SRP client with the accessory's salt and public key, then computes
|
|
3230
|
+
* the client's public key (A) and proof (M1) which proves knowledge of the PIN.
|
|
3231
|
+
*/
|
|
3204
3232
|
async m2(m1, pin = AIRPLAY_TRANSIENT_PIN) {
|
|
3205
3233
|
const srpKey = await import_srp.SRP.genKey(32);
|
|
3206
3234
|
this.#srp = new import_srp.SrpClient(import_srp.SRP.params.hap, m1.salt, Buffer.from("Pair-Setup"), Buffer.from(pin), srpKey, true);
|
|
@@ -3210,6 +3238,12 @@ var AccessoryPair = class extends BasePairing {
|
|
|
3210
3238
|
proof: this.#srp.computeM1()
|
|
3211
3239
|
};
|
|
3212
3240
|
}
|
|
3241
|
+
/**
|
|
3242
|
+
* SRP Step 3: Sends the client's public key and proof; receives the server's proof.
|
|
3243
|
+
*
|
|
3244
|
+
* Sends: TLV with State=M3, client SRP public key (A), and client proof (M1).
|
|
3245
|
+
* Receives: Server's M2-proof, confirming the accessory also knows the PIN.
|
|
3246
|
+
*/
|
|
3213
3247
|
async m3(m2) {
|
|
3214
3248
|
const response = await this.#requestHandler("m3", TLV8.encode([
|
|
3215
3249
|
[TLV8.Value.State, TLV8.State.M3],
|
|
@@ -3218,10 +3252,26 @@ var AccessoryPair = class extends BasePairing {
|
|
|
3218
3252
|
]));
|
|
3219
3253
|
return { serverProof: this.tlv(response).get(TLV8.Value.Proof) };
|
|
3220
3254
|
}
|
|
3255
|
+
/**
|
|
3256
|
+
* SRP Step 4: Verifies the server's proof and derives the shared secret.
|
|
3257
|
+
*
|
|
3258
|
+
* Validates the server's M2-proof, then computes the SRP session key (K).
|
|
3259
|
+
* This shared secret is the root key for all subsequent HKDF derivations.
|
|
3260
|
+
*/
|
|
3221
3261
|
async m4(m3) {
|
|
3222
3262
|
this.#srp.checkM2(m3.serverProof);
|
|
3223
3263
|
return { sharedSecret: this.#srp.computeK() };
|
|
3224
3264
|
}
|
|
3265
|
+
/**
|
|
3266
|
+
* HAP Step 5: Encrypted credential exchange — controller sends its identity.
|
|
3267
|
+
*
|
|
3268
|
+
* Derives a signing key (HKDF with "Pair-Setup-Controller-Sign-Salt/Info") and
|
|
3269
|
+
* a session key (HKDF with "Pair-Setup-Encrypt-Salt/Info") from the SRP shared secret.
|
|
3270
|
+
* Signs [signingKey || pairingId || Ed25519PublicKey] and sends it encrypted
|
|
3271
|
+
* via ChaCha20-Poly1305 (nonce "PS-Msg05").
|
|
3272
|
+
*
|
|
3273
|
+
* Receives: Encrypted accessory credentials (decrypted in M6).
|
|
3274
|
+
*/
|
|
3225
3275
|
async m5(m4) {
|
|
3226
3276
|
const iosDeviceX = hkdf({
|
|
3227
3277
|
hash: "sha512",
|
|
@@ -3260,6 +3310,15 @@ var AccessoryPair = class extends BasePairing {
|
|
|
3260
3310
|
sessionKey
|
|
3261
3311
|
};
|
|
3262
3312
|
}
|
|
3313
|
+
/**
|
|
3314
|
+
* HAP Step 6: Verifies the accessory's identity and extracts long-term credentials.
|
|
3315
|
+
*
|
|
3316
|
+
* Decrypts the accessory's response from M5 via ChaCha20-Poly1305 (nonce "PS-Msg06").
|
|
3317
|
+
* Derives "Accessory X" (HKDF with "Pair-Setup-Accessory-Sign-Salt/Info"), then
|
|
3318
|
+
* verifies the Ed25519 signature over [accessoryX || accessoryId || accessoryPublicKey].
|
|
3319
|
+
*
|
|
3320
|
+
* Returns the accessory's long-term public key and identifier for future Pair-Verify sessions.
|
|
3321
|
+
*/
|
|
3263
3322
|
async m6(m4, m5) {
|
|
3264
3323
|
const data = Chacha20.decrypt(m5.sessionKey, Buffer.from("PS-Msg06"), null, m5.data, m5.authTag);
|
|
3265
3324
|
const tlv = TLV8.decode(data);
|
|
@@ -3288,6 +3347,15 @@ var AccessoryPair = class extends BasePairing {
|
|
|
3288
3347
|
};
|
|
3289
3348
|
}
|
|
3290
3349
|
};
|
|
3350
|
+
/**
|
|
3351
|
+
* Implements the HAP (HomeKit Accessory Protocol) Pair-Verify flow using Curve25519 ECDH.
|
|
3352
|
+
*
|
|
3353
|
+
* Used to re-authenticate a previously paired accessory without needing the PIN again.
|
|
3354
|
+
* Establishes a new session with forward secrecy via ephemeral Curve25519 key exchange.
|
|
3355
|
+
*
|
|
3356
|
+
* Flow: M1 (ephemeral key exchange) → M2 (signature verification)
|
|
3357
|
+
* → M3 (controller proof) → M4 (session key derivation)
|
|
3358
|
+
*/
|
|
3291
3359
|
var AccessoryVerify = class extends BasePairing {
|
|
3292
3360
|
#ephemeralKeyPair;
|
|
3293
3361
|
#requestHandler;
|
|
@@ -3302,6 +3370,12 @@ var AccessoryVerify = class extends BasePairing {
|
|
|
3302
3370
|
await this.#m3(credentials.pairingId, credentials.secretKey, m2);
|
|
3303
3371
|
return await this.#m4(m2, credentials.pairingId);
|
|
3304
3372
|
}
|
|
3373
|
+
/**
|
|
3374
|
+
* Pair-Verify Step 1: Ephemeral Curve25519 key exchange initiation.
|
|
3375
|
+
*
|
|
3376
|
+
* Sends: TLV with State=M1 and controller's ephemeral Curve25519 public key.
|
|
3377
|
+
* Receives: Accessory's ephemeral public key and encrypted identity proof.
|
|
3378
|
+
*/
|
|
3305
3379
|
async #m1() {
|
|
3306
3380
|
const response = await this.#requestHandler("m1", TLV8.encode([[TLV8.Value.State, TLV8.State.M1], [TLV8.Value.PublicKey, Buffer.from(this.#ephemeralKeyPair.publicKey)]]));
|
|
3307
3381
|
const data = this.tlv(response);
|
|
@@ -3311,6 +3385,16 @@ var AccessoryVerify = class extends BasePairing {
|
|
|
3311
3385
|
serverPublicKey
|
|
3312
3386
|
};
|
|
3313
3387
|
}
|
|
3388
|
+
/**
|
|
3389
|
+
* Pair-Verify Step 2: ECDH shared secret derivation and accessory signature verification.
|
|
3390
|
+
*
|
|
3391
|
+
* Computes the Curve25519 shared secret, derives a session key via HKDF
|
|
3392
|
+
* ("Pair-Verify-Encrypt-Salt/Info"), then decrypts the accessory's response
|
|
3393
|
+
* via ChaCha20-Poly1305 (nonce "PV-Msg02").
|
|
3394
|
+
*
|
|
3395
|
+
* Verifies the accessory's Ed25519 signature over [serverPubKey || accessoryId || clientPubKey]
|
|
3396
|
+
* and checks the accessory identifier matches the stored credentials.
|
|
3397
|
+
*/
|
|
3314
3398
|
async #m2(localAccessoryIdentifier, longTermPublicKey, m1) {
|
|
3315
3399
|
const sharedSecret = Buffer.from(Curve25519.generateSharedSecKey(this.#ephemeralKeyPair.secretKey, m1.serverPublicKey));
|
|
3316
3400
|
const sessionKey = hkdf({
|
|
@@ -3339,6 +3423,13 @@ var AccessoryVerify = class extends BasePairing {
|
|
|
3339
3423
|
sharedSecret
|
|
3340
3424
|
};
|
|
3341
3425
|
}
|
|
3426
|
+
/**
|
|
3427
|
+
* Pair-Verify Step 3: Controller authentication proof.
|
|
3428
|
+
*
|
|
3429
|
+
* Signs [clientEphemeralPubKey || pairingId || serverEphemeralPubKey] with the
|
|
3430
|
+
* controller's long-term Ed25519 secret key. Encrypts the signed TLV via
|
|
3431
|
+
* ChaCha20-Poly1305 (nonce "PV-Msg03") and sends it to the accessory.
|
|
3432
|
+
*/
|
|
3342
3433
|
async #m3(pairingId, secretKey, m2) {
|
|
3343
3434
|
const iosDeviceInfo = Buffer.concat([
|
|
3344
3435
|
this.#ephemeralKeyPair.publicKey,
|
|
@@ -3352,6 +3443,12 @@ var AccessoryVerify = class extends BasePairing {
|
|
|
3352
3443
|
await this.#requestHandler("m3", TLV8.encode([[TLV8.Value.State, TLV8.State.M3], [TLV8.Value.EncryptedData, encrypted]]));
|
|
3353
3444
|
return {};
|
|
3354
3445
|
}
|
|
3446
|
+
/**
|
|
3447
|
+
* Pair-Verify Step 4: Returns the established session keys.
|
|
3448
|
+
*
|
|
3449
|
+
* The shared secret from the ECDH exchange is used by the caller to derive
|
|
3450
|
+
* encryption keys (via HKDF with "Control-Salt" and "Control-Read/Write-Encryption-Key").
|
|
3451
|
+
*/
|
|
3355
3452
|
async #m4(m2, pairingId) {
|
|
3356
3453
|
return {
|
|
3357
3454
|
accessoryToControllerKey: Buffer.alloc(0),
|
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.9.
|
|
4
|
+
"version": "0.9.17",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": {
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@basmilius/apple-encoding": "0.9.
|
|
49
|
-
"@basmilius/apple-encryption": "0.9.
|
|
48
|
+
"@basmilius/apple-encoding": "0.9.17",
|
|
49
|
+
"@basmilius/apple-encryption": "0.9.17"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/bun": "^1.3.11",
|