@lox-audioserver/node-airplay-sender 0.4.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/README.md +93 -0
- package/dist/core/ap2_test.d.ts +1 -0
- package/dist/core/ap2_test.js +8 -0
- package/dist/core/atv.d.ts +16 -0
- package/dist/core/atv.js +215 -0
- package/dist/core/atvAuthenticator.d.ts +30 -0
- package/dist/core/atvAuthenticator.js +134 -0
- package/dist/core/audioOut.d.ts +43 -0
- package/dist/core/audioOut.js +220 -0
- package/dist/core/deviceAirtunes.d.ts +76 -0
- package/dist/core/deviceAirtunes.js +536 -0
- package/dist/core/devices.d.ts +50 -0
- package/dist/core/devices.js +221 -0
- package/dist/core/index.d.ts +56 -0
- package/dist/core/index.js +144 -0
- package/dist/core/rtsp.d.ts +12 -0
- package/dist/core/rtsp.js +1678 -0
- package/dist/core/srp.d.ts +14 -0
- package/dist/core/srp.js +128 -0
- package/dist/core/udpServers.d.ts +44 -0
- package/dist/core/udpServers.js +244 -0
- package/dist/esm/core/ap2_test.js +8 -0
- package/dist/esm/core/atv.js +215 -0
- package/dist/esm/core/atvAuthenticator.js +134 -0
- package/dist/esm/core/audioOut.js +220 -0
- package/dist/esm/core/deviceAirtunes.js +536 -0
- package/dist/esm/core/devices.js +221 -0
- package/dist/esm/core/index.js +144 -0
- package/dist/esm/core/rtsp.js +1678 -0
- package/dist/esm/core/srp.js +128 -0
- package/dist/esm/core/udpServers.js +244 -0
- package/dist/esm/homekit/credentials.js +109 -0
- package/dist/esm/homekit/encryption.js +82 -0
- package/dist/esm/homekit/number.js +47 -0
- package/dist/esm/homekit/tlv.js +97 -0
- package/dist/esm/index.js +310 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/utils/alac.js +62 -0
- package/dist/esm/utils/alacEncoder.js +34 -0
- package/dist/esm/utils/circularBuffer.js +132 -0
- package/dist/esm/utils/config.js +49 -0
- package/dist/esm/utils/http.js +148 -0
- package/dist/esm/utils/ntp.js +56 -0
- package/dist/esm/utils/numUtil.js +17 -0
- package/dist/esm/utils/packetPool.js +52 -0
- package/dist/esm/utils/util.js +9 -0
- package/dist/homekit/credentials.d.ts +31 -0
- package/dist/homekit/credentials.js +109 -0
- package/dist/homekit/encryption.d.ts +12 -0
- package/dist/homekit/encryption.js +82 -0
- package/dist/homekit/number.d.ts +7 -0
- package/dist/homekit/number.js +47 -0
- package/dist/homekit/tlv.d.ts +25 -0
- package/dist/homekit/tlv.js +97 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +310 -0
- package/dist/utils/alac.d.ts +9 -0
- package/dist/utils/alac.js +62 -0
- package/dist/utils/alacEncoder.d.ts +14 -0
- package/dist/utils/alacEncoder.js +34 -0
- package/dist/utils/circularBuffer.d.ts +32 -0
- package/dist/utils/circularBuffer.js +132 -0
- package/dist/utils/config.d.ts +42 -0
- package/dist/utils/config.js +49 -0
- package/dist/utils/http.d.ts +19 -0
- package/dist/utils/http.js +148 -0
- package/dist/utils/ntp.d.ts +21 -0
- package/dist/utils/ntp.js +56 -0
- package/dist/utils/numUtil.d.ts +5 -0
- package/dist/utils/numUtil.js +17 -0
- package/dist/utils/packetPool.d.ts +25 -0
- package/dist/utils/packetPool.js +52 -0
- package/dist/utils/util.d.ts +2 -0
- package/dist/utils/util.js +9 -0
- package/package.json +71 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
7
|
+
const HttpMessage = (parseStartLine, writeStartLine) => {
|
|
8
|
+
const instance = {
|
|
9
|
+
parse: () => ({ headers: {} }),
|
|
10
|
+
write: () => Buffer.alloc(0),
|
|
11
|
+
};
|
|
12
|
+
instance.parse = (buffer) => {
|
|
13
|
+
const messageObject = { headers: {} };
|
|
14
|
+
// ...
|
|
15
|
+
let bodyIndex = buffer.indexOf('\r\n\r\n');
|
|
16
|
+
let headerString = buffer.slice(0, bodyIndex).toString();
|
|
17
|
+
let body = buffer.slice(bodyIndex + 4);
|
|
18
|
+
headerString = headerString.replace(/\r\n/g, '\n');
|
|
19
|
+
const lines = headerString.split('\n');
|
|
20
|
+
bodyIndex += 2;
|
|
21
|
+
// ...
|
|
22
|
+
let line = lines.shift();
|
|
23
|
+
if (line) {
|
|
24
|
+
parseStartLine(line, messageObject);
|
|
25
|
+
}
|
|
26
|
+
// ...
|
|
27
|
+
line = lines.shift();
|
|
28
|
+
while (line) {
|
|
29
|
+
const headerName = line.substr(0, line.indexOf(':'));
|
|
30
|
+
const headerValue = line.substr(line.indexOf(':') + 1);
|
|
31
|
+
messageObject.headers[headerName] = headerValue.trim();
|
|
32
|
+
line = lines.shift();
|
|
33
|
+
}
|
|
34
|
+
// ...
|
|
35
|
+
if (messageObject.headers['Content-Length'] && messageObject.headers['Content-Length'] !== '0') {
|
|
36
|
+
messageObject.body = body;
|
|
37
|
+
}
|
|
38
|
+
return messageObject;
|
|
39
|
+
};
|
|
40
|
+
instance.write = (messageObject) => {
|
|
41
|
+
let messageString = writeStartLine(messageObject);
|
|
42
|
+
messageString += '\r\n';
|
|
43
|
+
if (messageObject.body) {
|
|
44
|
+
messageObject.headers['Content-Length'] = String(Buffer.byteLength(messageObject.body));
|
|
45
|
+
}
|
|
46
|
+
for (const header in messageObject.headers) {
|
|
47
|
+
messageString += `${header}: ${messageObject.headers[header]}\r\n`;
|
|
48
|
+
}
|
|
49
|
+
messageString += '\r\n';
|
|
50
|
+
const buffer = Buffer.from(messageString);
|
|
51
|
+
if (!messageObject.body) {
|
|
52
|
+
return buffer;
|
|
53
|
+
}
|
|
54
|
+
return Buffer.concat([buffer, messageObject.body], buffer.length + messageObject.body.length);
|
|
55
|
+
};
|
|
56
|
+
return instance;
|
|
57
|
+
};
|
|
58
|
+
const HttpRequest = () => HttpMessage(() => { }, // currently not parsing requests.
|
|
59
|
+
(messageObject) => `${messageObject.method} ${messageObject.path} HTTP/1.1`);
|
|
60
|
+
const HttpResponse = () => HttpMessage((line, messageObject) => {
|
|
61
|
+
messageObject.statusCode = parseInt(line.split(' ')[1], 10);
|
|
62
|
+
}, () => '');
|
|
63
|
+
// ...
|
|
64
|
+
class HttpClient {
|
|
65
|
+
resolveQueue = [];
|
|
66
|
+
pendingResponse = null;
|
|
67
|
+
socket;
|
|
68
|
+
host;
|
|
69
|
+
// ....
|
|
70
|
+
parseResponse(data) {
|
|
71
|
+
const res = HttpResponse().parse(data);
|
|
72
|
+
if (res.headers['Content-Length'] && Number(res.headers['Content-Length']) > 0) {
|
|
73
|
+
const remaining = Number(res.headers['Content-Length']) - (res.body?.byteLength ?? 0);
|
|
74
|
+
if (remaining > 0) {
|
|
75
|
+
// not all data for this response's corresponding request was read. Create a pending response object
|
|
76
|
+
// to use for further reads.
|
|
77
|
+
this.pendingResponse = {
|
|
78
|
+
res,
|
|
79
|
+
remaining
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!this.pendingResponse) {
|
|
84
|
+
const rr = this.resolveQueue.shift();
|
|
85
|
+
if (!rr)
|
|
86
|
+
return;
|
|
87
|
+
res.statusCode === 200
|
|
88
|
+
? rr.resolve(res)
|
|
89
|
+
: rr.resolve(null);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// ...
|
|
93
|
+
connect(host, port = 80) {
|
|
94
|
+
this.host = host;
|
|
95
|
+
return new Promise(resolve => {
|
|
96
|
+
this.socket = node_net_1.default.connect({
|
|
97
|
+
host,
|
|
98
|
+
port
|
|
99
|
+
}, resolve);
|
|
100
|
+
this.socket.on('data', data => {
|
|
101
|
+
if (!this.pendingResponse) {
|
|
102
|
+
// there is no response pending, parse the data.
|
|
103
|
+
this.parseResponse(data);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// incoming data for the pending response.
|
|
107
|
+
const existing = this.pendingResponse.res.body ?? Buffer.alloc(0);
|
|
108
|
+
this.pendingResponse.res.body = Buffer.concat([existing, data], data.byteLength + existing.byteLength);
|
|
109
|
+
this.pendingResponse.remaining -= data.byteLength;
|
|
110
|
+
if (this.pendingResponse.remaining === 0) {
|
|
111
|
+
// all remaining data for the pending response has been read; resolve the promise for the
|
|
112
|
+
// corresponding request.
|
|
113
|
+
const rr = this.resolveQueue.shift();
|
|
114
|
+
if (!rr) {
|
|
115
|
+
this.pendingResponse = null;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.pendingResponse.res.statusCode === 200
|
|
119
|
+
? rr.resolve(this.pendingResponse.res)
|
|
120
|
+
: rr.reject(new Error(`HTTP status: ${this.pendingResponse.res.statusCode}`));
|
|
121
|
+
this.pendingResponse = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
request(method, path, headers, body) {
|
|
128
|
+
headers = headers || {};
|
|
129
|
+
// headers['Host'] = `${this.host}:${this.socket.remotePort}`;
|
|
130
|
+
const data = HttpRequest().write({
|
|
131
|
+
method,
|
|
132
|
+
path,
|
|
133
|
+
headers,
|
|
134
|
+
body
|
|
135
|
+
});
|
|
136
|
+
// ...
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
this.resolveQueue.push({ resolve, reject });
|
|
139
|
+
this.socket?.write(data);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
close() {
|
|
143
|
+
this.socket?.end();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ...
|
|
147
|
+
const createHttpClient = () => new HttpClient();
|
|
148
|
+
exports.default = createHttpClient;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.toNtpTimestamp = toNtpTimestamp;
|
|
7
|
+
exports.parseNtpTimestamp = parseNtpTimestamp;
|
|
8
|
+
exports.ntpFromUnixMs = ntpFromUnixMs;
|
|
9
|
+
const config_1 = __importDefault(require("./config"));
|
|
10
|
+
const NS_PER_SEC = 1000000000n;
|
|
11
|
+
const FRAC_PER_SEC = 0x100000000n; // 2^32
|
|
12
|
+
class NTP {
|
|
13
|
+
/** Convert monotonic clock to NTP epoch (1900-01-01) seconds + fractional. */
|
|
14
|
+
timestamp() {
|
|
15
|
+
const nowNs = process.hrtime.bigint();
|
|
16
|
+
const sec = Number(nowNs / NS_PER_SEC) + config_1.default.ntp_epoch;
|
|
17
|
+
const frac = Number(((nowNs % NS_PER_SEC) * FRAC_PER_SEC) / NS_PER_SEC);
|
|
18
|
+
const ts = Buffer.alloc(8);
|
|
19
|
+
ts.writeUInt32BE(sec >>> 0, 0);
|
|
20
|
+
ts.writeUInt32BE(frac >>> 0, 4);
|
|
21
|
+
return ts;
|
|
22
|
+
}
|
|
23
|
+
/** Return the current NTP fractional component (for compatibility). */
|
|
24
|
+
getTime() {
|
|
25
|
+
const nowNs = process.hrtime.bigint();
|
|
26
|
+
return Number(((nowNs % NS_PER_SEC) * FRAC_PER_SEC) / NS_PER_SEC);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.default = new NTP();
|
|
30
|
+
/** Pack various NTP timestamp representations into a uint64 bigint (sec<<32|frac). */
|
|
31
|
+
function toNtpTimestamp(input) {
|
|
32
|
+
if (typeof input === 'bigint')
|
|
33
|
+
return input;
|
|
34
|
+
if (typeof input === 'number')
|
|
35
|
+
return BigInt(input);
|
|
36
|
+
const sec = BigInt(input.sec >>> 0);
|
|
37
|
+
const frac = BigInt(input.frac >>> 0);
|
|
38
|
+
return (sec << 32n) | frac;
|
|
39
|
+
}
|
|
40
|
+
/** Parse an NTP timestamp (uint64 or sec/frac) into components. */
|
|
41
|
+
function parseNtpTimestamp(input) {
|
|
42
|
+
if (typeof input === 'bigint' || typeof input === 'number') {
|
|
43
|
+
const value = typeof input === 'bigint' ? input : BigInt(input);
|
|
44
|
+
const sec = Number(value >> 32n);
|
|
45
|
+
const frac = Number(value & 0xffffffffn);
|
|
46
|
+
return { sec, frac };
|
|
47
|
+
}
|
|
48
|
+
return { sec: input.sec, frac: input.frac };
|
|
49
|
+
}
|
|
50
|
+
/** Build an NTP timestamp from a Unix epoch (ms). */
|
|
51
|
+
function ntpFromUnixMs(unixMs) {
|
|
52
|
+
const sec = Math.floor(unixMs / 1000) + config_1.default.ntp_epoch;
|
|
53
|
+
const ms = unixMs % 1000;
|
|
54
|
+
const frac = Math.floor((ms / 1000) * Number(FRAC_PER_SEC));
|
|
55
|
+
return (BigInt(sec >>> 0) << 32n) | BigInt(frac >>> 0);
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.low32 = exports.low16 = exports.randomInt = exports.randomBase64 = exports.randomHex = void 0;
|
|
7
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
|
+
const randomHex = (n) => node_crypto_1.default.randomBytes(n).toString('hex');
|
|
9
|
+
exports.randomHex = randomHex;
|
|
10
|
+
const randomBase64 = (n) => node_crypto_1.default.randomBytes(n).toString('base64').replace('=', '');
|
|
11
|
+
exports.randomBase64 = randomBase64;
|
|
12
|
+
const randomInt = (n) => Math.floor(Math.random() * Math.pow(10, n));
|
|
13
|
+
exports.randomInt = randomInt;
|
|
14
|
+
const low16 = (i) => Math.abs(i) % 65536;
|
|
15
|
+
exports.low16 = low16;
|
|
16
|
+
const low32 = (i) => Math.abs(i) % 4294967296;
|
|
17
|
+
exports.low32 = low32;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Packet = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Reusable packet structure holding PCM/ALAC data plus sequence.
|
|
6
|
+
* Reference-counted to reduce allocations in the streaming path.
|
|
7
|
+
*/
|
|
8
|
+
class Packet {
|
|
9
|
+
pool;
|
|
10
|
+
ref = 1;
|
|
11
|
+
seq = null;
|
|
12
|
+
pcm;
|
|
13
|
+
constructor(pool, packetSize) {
|
|
14
|
+
this.pool = pool;
|
|
15
|
+
this.pcm = Buffer.alloc(packetSize);
|
|
16
|
+
}
|
|
17
|
+
/** Increment ref count when sharing the packet. */
|
|
18
|
+
retain() {
|
|
19
|
+
this.ref += 1;
|
|
20
|
+
}
|
|
21
|
+
/** Decrement ref count and return to pool when free. */
|
|
22
|
+
release() {
|
|
23
|
+
this.ref -= 1;
|
|
24
|
+
if (this.ref === 0) {
|
|
25
|
+
this.seq = null;
|
|
26
|
+
this.pool.release(this);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.Packet = Packet;
|
|
31
|
+
/** Simple pool of Packet instances to avoid GC pressure during streaming. */
|
|
32
|
+
class PacketPool {
|
|
33
|
+
packetSize;
|
|
34
|
+
pool = [];
|
|
35
|
+
constructor(packetSize) {
|
|
36
|
+
this.packetSize = packetSize;
|
|
37
|
+
}
|
|
38
|
+
/** Borrow a packet from the pool or allocate a new one. */
|
|
39
|
+
getPacket() {
|
|
40
|
+
const packet = this.pool.shift();
|
|
41
|
+
if (!packet) {
|
|
42
|
+
return new Packet(this, this.packetSize);
|
|
43
|
+
}
|
|
44
|
+
packet.retain();
|
|
45
|
+
return packet;
|
|
46
|
+
}
|
|
47
|
+
/** Return a packet to the pool. */
|
|
48
|
+
release(packet) {
|
|
49
|
+
this.pool.push(packet);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.default = PacketPool;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buf2hex = exports.hexString2ArrayBuffer = void 0;
|
|
4
|
+
const hexString2ArrayBuffer = (hexString) => new Uint8Array(hexString.match(/[\da-f]{2}/gi)?.map((h) => parseInt(h, 16)) ?? []);
|
|
5
|
+
exports.hexString2ArrayBuffer = hexString2ArrayBuffer;
|
|
6
|
+
const buf2hex = (buffer) => Array.prototype.map
|
|
7
|
+
.call(new Uint8Array(buffer), (x) => `00${x.toString(16)}`.slice(-2))
|
|
8
|
+
.join('');
|
|
9
|
+
exports.buf2hex = buf2hex;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds and serializes HomeKit credential blobs used during AirPlay 2 auth.
|
|
3
|
+
*/
|
|
4
|
+
declare class Credentials {
|
|
5
|
+
uniqueIdentifier: string;
|
|
6
|
+
identifier: Buffer;
|
|
7
|
+
pairingId: string;
|
|
8
|
+
publicKey: Buffer;
|
|
9
|
+
encryptionKey: Buffer;
|
|
10
|
+
encryptCount: number;
|
|
11
|
+
decryptCount: number;
|
|
12
|
+
writeKey: Buffer;
|
|
13
|
+
readKey: Buffer;
|
|
14
|
+
constructor(uniqueIdentifier: string, identifier: Buffer, pairingId: string, publicKey: Buffer, encryptionKey: Buffer);
|
|
15
|
+
/**
|
|
16
|
+
* Parse a credentials string into a Credentials object.
|
|
17
|
+
* @param text The credentials string.
|
|
18
|
+
* @returns A credentials object.
|
|
19
|
+
*/
|
|
20
|
+
static parse(text: string): Credentials;
|
|
21
|
+
/**
|
|
22
|
+
* Returns a string representation of a Credentials object.
|
|
23
|
+
* @returns A string representation of a Credentials object.
|
|
24
|
+
*/
|
|
25
|
+
toString(): string;
|
|
26
|
+
encrypt(message: Buffer): Buffer;
|
|
27
|
+
decrypt(message: Buffer): Buffer;
|
|
28
|
+
encryptAudio(message: Buffer, aad: Buffer | null, nonce: number): Buffer;
|
|
29
|
+
rotateKeys(): void;
|
|
30
|
+
}
|
|
31
|
+
export { Credentials };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Credentials = void 0;
|
|
7
|
+
const encryption_1 = __importDefault(require("./encryption"));
|
|
8
|
+
const struct = require('python-struct');
|
|
9
|
+
/**
|
|
10
|
+
* Holds and serializes HomeKit credential blobs used during AirPlay 2 auth.
|
|
11
|
+
*/
|
|
12
|
+
class Credentials {
|
|
13
|
+
uniqueIdentifier;
|
|
14
|
+
identifier;
|
|
15
|
+
pairingId;
|
|
16
|
+
publicKey;
|
|
17
|
+
encryptionKey;
|
|
18
|
+
encryptCount;
|
|
19
|
+
decryptCount;
|
|
20
|
+
writeKey;
|
|
21
|
+
readKey;
|
|
22
|
+
constructor(uniqueIdentifier, identifier, pairingId, publicKey, encryptionKey) {
|
|
23
|
+
this.uniqueIdentifier = uniqueIdentifier;
|
|
24
|
+
this.identifier = identifier;
|
|
25
|
+
this.pairingId = pairingId;
|
|
26
|
+
this.publicKey = publicKey;
|
|
27
|
+
this.encryptionKey = encryptionKey;
|
|
28
|
+
this.encryptCount = 0;
|
|
29
|
+
this.decryptCount = 0;
|
|
30
|
+
this.writeKey = encryptionKey;
|
|
31
|
+
this.readKey = encryptionKey;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse a credentials string into a Credentials object.
|
|
35
|
+
* @param text The credentials string.
|
|
36
|
+
* @returns A credentials object.
|
|
37
|
+
*/
|
|
38
|
+
static parse(text) {
|
|
39
|
+
const parts = text.split(':');
|
|
40
|
+
return new Credentials(parts[0], Buffer.from(parts[1], 'hex'), Buffer.from(parts[2], 'hex').toString(), Buffer.from(parts[3], 'hex'), Buffer.from(parts[4], 'hex'));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns a string representation of a Credentials object.
|
|
44
|
+
* @returns A string representation of a Credentials object.
|
|
45
|
+
*/
|
|
46
|
+
toString() {
|
|
47
|
+
return this.uniqueIdentifier
|
|
48
|
+
+ ":"
|
|
49
|
+
+ this.identifier.toString('hex')
|
|
50
|
+
+ ":"
|
|
51
|
+
+ Buffer.from(this.pairingId).toString('hex')
|
|
52
|
+
+ ":"
|
|
53
|
+
+ this.publicKey.toString('hex')
|
|
54
|
+
+ ":"
|
|
55
|
+
+ this.encryptionKey.toString('hex');
|
|
56
|
+
}
|
|
57
|
+
encrypt(message) {
|
|
58
|
+
let offset = 0;
|
|
59
|
+
const total = message.byteLength;
|
|
60
|
+
let result = Buffer.concat([]);
|
|
61
|
+
while (offset < total) {
|
|
62
|
+
const length = Math.min(total - offset, 1024);
|
|
63
|
+
const s1lengthBytes = struct.pack("H", length);
|
|
64
|
+
// let cipher = crypto.createCipheriv('chacha20-poly1305', this.writeKey, Buffer.concat([Buffer.from([0x00,0x00,0x00,0x00]),struct.pack("Q", this.decryptCount)]), { authTagLength: 16 });
|
|
65
|
+
// cipher.setAAD(s1length_bytes);
|
|
66
|
+
// let s1ct = cipher.update(message);
|
|
67
|
+
// cipher.final();
|
|
68
|
+
// let s1tag = encryption_1.default.computePoly1305(s1ct,s1length_bytes,Buffer.concat([Buffer.from([0x00,0x00,0x00,0x00]),struct.pack("Q", this.decryptCount)]),this.writeKey)
|
|
69
|
+
const [s1ct, s1tag] = encryption_1.default.encryptAndSeal(message.slice(offset, offset + length), s1lengthBytes, Buffer.concat([Buffer.from([0x00, 0x00, 0x00, 0x00]), struct.pack("Q", this.encryptCount)]), this.writeKey);
|
|
70
|
+
const ciphertext = Buffer.concat([s1lengthBytes, s1ct, s1tag]);
|
|
71
|
+
offset += length;
|
|
72
|
+
this.encryptCount += 1;
|
|
73
|
+
result = Buffer.concat([result, ciphertext]);
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
decrypt(message) {
|
|
78
|
+
let offset = 0;
|
|
79
|
+
let result = Buffer.concat([]);
|
|
80
|
+
while (offset < message.byteLength) {
|
|
81
|
+
const lengthBytes = message.slice(offset, offset + 2);
|
|
82
|
+
const length = struct.unpack("H", lengthBytes);
|
|
83
|
+
const messagea = message.slice(offset + 2, offset + 2 + length[0] + 16);
|
|
84
|
+
const cipherText = messagea.slice(0, -16);
|
|
85
|
+
const hmac = messagea.slice(-16);
|
|
86
|
+
const decrypted = encryption_1.default.verifyAndDecrypt(cipherText, hmac, lengthBytes, Buffer.concat([Buffer.from([0x00, 0x00, 0x00, 0x00]), struct.pack("Q", this.decryptCount)]), this.readKey);
|
|
87
|
+
this.decryptCount += 1;
|
|
88
|
+
offset = offset + length[0] + 16 + 2;
|
|
89
|
+
result = Buffer.concat([result, decrypted ?? Buffer.alloc(0)]);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
encryptAudio(message, aad, nonce) {
|
|
94
|
+
return Buffer.concat([
|
|
95
|
+
Buffer.concat(encryption_1.default.encryptAndSeal(message, aad, struct.pack("Q", nonce), this.writeKey)),
|
|
96
|
+
Buffer.from(struct.pack("Q", nonce)),
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
rotateKeys() {
|
|
100
|
+
const info = Buffer.from('AirPlay:Audio', 'utf-8'); // reference-style info
|
|
101
|
+
const salt = Buffer.alloc(16, 0); // deterministic salt like reference
|
|
102
|
+
const newKey = encryption_1.default.HKDF('sha256', salt, this.encryptionKey, info, 32);
|
|
103
|
+
this.writeKey = newKey;
|
|
104
|
+
this.readKey = newKey;
|
|
105
|
+
this.encryptCount = 0;
|
|
106
|
+
this.decryptCount = 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.Credentials = Credentials;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AirPlay 2/HomeKit encryption helpers (ChaCha20-Poly1305 + HKDF) ported from node_airtunes2.
|
|
3
|
+
*/
|
|
4
|
+
declare function verifyAndDecrypt(cipherText: Buffer, mac: Buffer, AAD: Buffer | null, nonce: Buffer, key: Buffer): Buffer | null;
|
|
5
|
+
declare function encryptAndSeal(plainText: Buffer, AAD: Buffer | null, nonce: Buffer, key: Buffer): [Buffer, Buffer];
|
|
6
|
+
declare function HKDF(hashAlg: string, salt: Buffer, ikm: Buffer, info: Buffer | string, size: number): Buffer;
|
|
7
|
+
declare const _default: {
|
|
8
|
+
encryptAndSeal: typeof encryptAndSeal;
|
|
9
|
+
verifyAndDecrypt: typeof verifyAndDecrypt;
|
|
10
|
+
HKDF: typeof HKDF;
|
|
11
|
+
};
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
7
|
+
/**
|
|
8
|
+
* AirPlay 2/HomeKit encryption helpers (ChaCha20-Poly1305 + HKDF) ported from node_airtunes2.
|
|
9
|
+
*/
|
|
10
|
+
// i'd really prefer for this to be a direct call to
|
|
11
|
+
// Sodium.crypto_aead_chacha20poly1305_decrypt()
|
|
12
|
+
// but unfortunately the way it constructs the message to
|
|
13
|
+
// calculate the HMAC is not compatible with homekit
|
|
14
|
+
// (long story short, it uses [ AAD, AAD.length, CipherText, CipherText.length ]
|
|
15
|
+
// whereas homekit expects [ AAD, CipherText, AAD.length, CipherText.length ]
|
|
16
|
+
function verifyAndDecrypt(cipherText, mac, AAD, nonce, key) {
|
|
17
|
+
try {
|
|
18
|
+
let nonceBuf = nonce;
|
|
19
|
+
if (nonceBuf.byteLength === 8) {
|
|
20
|
+
nonceBuf = Buffer.concat([Buffer.from([0x00, 0x00, 0x00, 0x00]), nonceBuf]);
|
|
21
|
+
}
|
|
22
|
+
const decipher = crypto_1.default.createDecipheriv('chacha20-poly1305', key, nonceBuf, { authTagLength: 16 });
|
|
23
|
+
if (AAD != null) {
|
|
24
|
+
decipher.setAAD(AAD); // must be called before data
|
|
25
|
+
}
|
|
26
|
+
decipher.setAuthTag(mac);
|
|
27
|
+
const decrypted = Buffer.concat([decipher.update(cipherText), decipher.final()]);
|
|
28
|
+
return decrypted;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function encryptAndSeal(plainText, AAD, nonce, key) {
|
|
35
|
+
let nonceBuf = nonce;
|
|
36
|
+
if (nonceBuf.byteLength === 8) {
|
|
37
|
+
nonceBuf = Buffer.concat([Buffer.from([0x00, 0x00, 0x00, 0x00]), nonceBuf]);
|
|
38
|
+
}
|
|
39
|
+
const cipher = crypto_1.default.createCipheriv('chacha20-poly1305', key, nonceBuf, { authTagLength: 16 });
|
|
40
|
+
if (AAD != null) {
|
|
41
|
+
cipher.setAAD(AAD); // must be called before data
|
|
42
|
+
}
|
|
43
|
+
const cipherText = Buffer.concat([cipher.update(plainText), cipher.final()]);
|
|
44
|
+
const hmac = cipher.getAuthTag();
|
|
45
|
+
return [cipherText, hmac];
|
|
46
|
+
}
|
|
47
|
+
// function getPadding(buffer, blockSize) {
|
|
48
|
+
// return buffer.length % blockSize === 0
|
|
49
|
+
// ? Buffer.alloc(0)
|
|
50
|
+
// : Buffer.alloc(blockSize - (buffer.length % blockSize));
|
|
51
|
+
// }
|
|
52
|
+
function HKDF(hashAlg, salt, ikm, info, size) {
|
|
53
|
+
// create the hash alg to see if it exists and get its length
|
|
54
|
+
const hash = crypto_1.default.createHash(hashAlg);
|
|
55
|
+
const hashLength = hash.digest().length;
|
|
56
|
+
// now we compute the PRK
|
|
57
|
+
const hmac = crypto_1.default.createHmac(hashAlg, salt);
|
|
58
|
+
hmac.update(ikm);
|
|
59
|
+
const prk = hmac.digest();
|
|
60
|
+
let prev = Buffer.alloc(0);
|
|
61
|
+
const buffers = [];
|
|
62
|
+
const numBlocks = Math.ceil(size / hashLength);
|
|
63
|
+
const infoBuf = Buffer.from(info);
|
|
64
|
+
for (let i = 0; i < numBlocks; i++) {
|
|
65
|
+
const roundHmac = crypto_1.default.createHmac(hashAlg, prk);
|
|
66
|
+
const input = Buffer.concat([
|
|
67
|
+
prev,
|
|
68
|
+
infoBuf,
|
|
69
|
+
Buffer.from(String.fromCharCode(i + 1)),
|
|
70
|
+
]);
|
|
71
|
+
roundHmac.update(input);
|
|
72
|
+
prev = roundHmac.digest();
|
|
73
|
+
buffers.push(prev);
|
|
74
|
+
}
|
|
75
|
+
const output = Buffer.concat(buffers, size);
|
|
76
|
+
return output.slice(0, size);
|
|
77
|
+
}
|
|
78
|
+
exports.default = {
|
|
79
|
+
encryptAndSeal,
|
|
80
|
+
verifyAndDecrypt,
|
|
81
|
+
HKDF,
|
|
82
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const assert_1 = __importDefault(require("assert"));
|
|
7
|
+
/*
|
|
8
|
+
* Originally based on code from github:KhaosT/HAP-NodeJS@0c8fd88 used
|
|
9
|
+
* used per the terms of the Apache Software License v2.
|
|
10
|
+
*
|
|
11
|
+
* Original code copyright Khaos Tian <khaos.tian@gmail.com>
|
|
12
|
+
*
|
|
13
|
+
* Modifications copyright Zach Bean <zb@forty2.com>
|
|
14
|
+
* * Reformatted for ES6-style module
|
|
15
|
+
* * renamed *UInt64* to *UInt53* to be more clear about range
|
|
16
|
+
* * renamed uintHighLow to be more clear about what it does
|
|
17
|
+
* * Refactored to return a buffer rather write into a passed-in buffer
|
|
18
|
+
*/
|
|
19
|
+
function splitUInt53(value) {
|
|
20
|
+
const MAX_UINT32 = 0x00000000ffffffff;
|
|
21
|
+
const MAX_INT53 = 0x001fffffffffffff;
|
|
22
|
+
(0, assert_1.default)(value > -1 && value <= MAX_INT53, 'number out of range');
|
|
23
|
+
(0, assert_1.default)(Math.floor(value) === value, 'number must be an integer');
|
|
24
|
+
let high = 0;
|
|
25
|
+
const signbit = value & 0xffffffff;
|
|
26
|
+
const low = signbit < 0 ? (value & 0x7fffffff) + 0x80000000 : signbit;
|
|
27
|
+
if (value > MAX_UINT32) {
|
|
28
|
+
high = (value - low) / (MAX_UINT32 + 1);
|
|
29
|
+
}
|
|
30
|
+
return [high, low];
|
|
31
|
+
}
|
|
32
|
+
function UInt53toBufferLE(value) {
|
|
33
|
+
const [high, low] = splitUInt53(value);
|
|
34
|
+
const buf = Buffer.alloc(8);
|
|
35
|
+
buf.writeUInt32LE(low, 0);
|
|
36
|
+
buf.writeUInt32LE(high, 4);
|
|
37
|
+
return buf;
|
|
38
|
+
}
|
|
39
|
+
function UInt16toBufferBE(value) {
|
|
40
|
+
const buf = Buffer.alloc(2);
|
|
41
|
+
buf.writeUInt16BE(value, 0);
|
|
42
|
+
return buf;
|
|
43
|
+
}
|
|
44
|
+
exports.default = {
|
|
45
|
+
UInt53toBufferLE,
|
|
46
|
+
UInt16toBufferBE,
|
|
47
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type TLVValue = Buffer | string | number;
|
|
2
|
+
type TLVMap = Record<number, Buffer>;
|
|
3
|
+
type TLVArgs = Array<number | TLVValue>;
|
|
4
|
+
declare function encode(type: number, data: TLVValue, ...args: TLVArgs): Buffer;
|
|
5
|
+
declare function decode(data: Buffer): TLVMap;
|
|
6
|
+
declare const _default: {
|
|
7
|
+
Tag: {
|
|
8
|
+
readonly PairingMethod: 0;
|
|
9
|
+
readonly Username: 1;
|
|
10
|
+
readonly Salt: 2;
|
|
11
|
+
readonly PublicKey: 3;
|
|
12
|
+
readonly Proof: 4;
|
|
13
|
+
readonly EncryptedData: 5;
|
|
14
|
+
readonly Sequence: 6;
|
|
15
|
+
readonly ErrorCode: 7;
|
|
16
|
+
readonly BackOff: 8;
|
|
17
|
+
readonly Signature: 10;
|
|
18
|
+
readonly MFiCertificate: 9;
|
|
19
|
+
readonly MFiSignature: 10;
|
|
20
|
+
readonly Flags: 19;
|
|
21
|
+
};
|
|
22
|
+
encode: typeof encode;
|
|
23
|
+
decode: typeof decode;
|
|
24
|
+
};
|
|
25
|
+
export default _default;
|