@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.
Files changed (75) hide show
  1. package/README.md +93 -0
  2. package/dist/core/ap2_test.d.ts +1 -0
  3. package/dist/core/ap2_test.js +8 -0
  4. package/dist/core/atv.d.ts +16 -0
  5. package/dist/core/atv.js +215 -0
  6. package/dist/core/atvAuthenticator.d.ts +30 -0
  7. package/dist/core/atvAuthenticator.js +134 -0
  8. package/dist/core/audioOut.d.ts +43 -0
  9. package/dist/core/audioOut.js +220 -0
  10. package/dist/core/deviceAirtunes.d.ts +76 -0
  11. package/dist/core/deviceAirtunes.js +536 -0
  12. package/dist/core/devices.d.ts +50 -0
  13. package/dist/core/devices.js +221 -0
  14. package/dist/core/index.d.ts +56 -0
  15. package/dist/core/index.js +144 -0
  16. package/dist/core/rtsp.d.ts +12 -0
  17. package/dist/core/rtsp.js +1678 -0
  18. package/dist/core/srp.d.ts +14 -0
  19. package/dist/core/srp.js +128 -0
  20. package/dist/core/udpServers.d.ts +44 -0
  21. package/dist/core/udpServers.js +244 -0
  22. package/dist/esm/core/ap2_test.js +8 -0
  23. package/dist/esm/core/atv.js +215 -0
  24. package/dist/esm/core/atvAuthenticator.js +134 -0
  25. package/dist/esm/core/audioOut.js +220 -0
  26. package/dist/esm/core/deviceAirtunes.js +536 -0
  27. package/dist/esm/core/devices.js +221 -0
  28. package/dist/esm/core/index.js +144 -0
  29. package/dist/esm/core/rtsp.js +1678 -0
  30. package/dist/esm/core/srp.js +128 -0
  31. package/dist/esm/core/udpServers.js +244 -0
  32. package/dist/esm/homekit/credentials.js +109 -0
  33. package/dist/esm/homekit/encryption.js +82 -0
  34. package/dist/esm/homekit/number.js +47 -0
  35. package/dist/esm/homekit/tlv.js +97 -0
  36. package/dist/esm/index.js +310 -0
  37. package/dist/esm/package.json +1 -0
  38. package/dist/esm/utils/alac.js +62 -0
  39. package/dist/esm/utils/alacEncoder.js +34 -0
  40. package/dist/esm/utils/circularBuffer.js +132 -0
  41. package/dist/esm/utils/config.js +49 -0
  42. package/dist/esm/utils/http.js +148 -0
  43. package/dist/esm/utils/ntp.js +56 -0
  44. package/dist/esm/utils/numUtil.js +17 -0
  45. package/dist/esm/utils/packetPool.js +52 -0
  46. package/dist/esm/utils/util.js +9 -0
  47. package/dist/homekit/credentials.d.ts +31 -0
  48. package/dist/homekit/credentials.js +109 -0
  49. package/dist/homekit/encryption.d.ts +12 -0
  50. package/dist/homekit/encryption.js +82 -0
  51. package/dist/homekit/number.d.ts +7 -0
  52. package/dist/homekit/number.js +47 -0
  53. package/dist/homekit/tlv.d.ts +25 -0
  54. package/dist/homekit/tlv.js +97 -0
  55. package/dist/index.d.ts +121 -0
  56. package/dist/index.js +310 -0
  57. package/dist/utils/alac.d.ts +9 -0
  58. package/dist/utils/alac.js +62 -0
  59. package/dist/utils/alacEncoder.d.ts +14 -0
  60. package/dist/utils/alacEncoder.js +34 -0
  61. package/dist/utils/circularBuffer.d.ts +32 -0
  62. package/dist/utils/circularBuffer.js +132 -0
  63. package/dist/utils/config.d.ts +42 -0
  64. package/dist/utils/config.js +49 -0
  65. package/dist/utils/http.d.ts +19 -0
  66. package/dist/utils/http.js +148 -0
  67. package/dist/utils/ntp.d.ts +21 -0
  68. package/dist/utils/ntp.js +56 -0
  69. package/dist/utils/numUtil.d.ts +5 -0
  70. package/dist/utils/numUtil.js +17 -0
  71. package/dist/utils/packetPool.d.ts +25 -0
  72. package/dist/utils/packetPool.js +52 -0
  73. package/dist/utils/util.d.ts +2 -0
  74. package/dist/utils/util.js +9 -0
  75. 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,7 @@
1
+ declare function UInt53toBufferLE(value: number): Buffer;
2
+ declare function UInt16toBufferBE(value: number): Buffer;
3
+ declare const _default: {
4
+ UInt53toBufferLE: typeof UInt53toBufferLE;
5
+ UInt16toBufferBE: typeof UInt16toBufferBE;
6
+ };
7
+ export default _default;
@@ -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;