lox-airplay-sender 0.1.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 +85 -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 +30 -0
- package/dist/core/audioOut.js +80 -0
- package/dist/core/deviceAirtunes.d.ts +72 -0
- package/dist/core/deviceAirtunes.js +501 -0
- package/dist/core/devices.d.ts +50 -0
- package/dist/core/devices.js +209 -0
- package/dist/core/index.d.ts +47 -0
- package/dist/core/index.js +97 -0
- package/dist/core/rtsp.d.ts +12 -0
- package/dist/core/rtsp.js +1590 -0
- package/dist/core/srp.d.ts +14 -0
- package/dist/core/srp.js +128 -0
- package/dist/core/udpServers.d.ts +26 -0
- package/dist/core/udpServers.js +149 -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 +80 -0
- package/dist/esm/core/deviceAirtunes.js +501 -0
- package/dist/esm/core/devices.js +209 -0
- package/dist/esm/core/index.js +97 -0
- package/dist/esm/core/rtsp.js +1590 -0
- package/dist/esm/core/srp.js +128 -0
- package/dist/esm/core/udpServers.js +149 -0
- package/dist/esm/homekit/credentials.js +100 -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 +265 -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 +124 -0
- package/dist/esm/utils/config.js +28 -0
- package/dist/esm/utils/http.js +148 -0
- package/dist/esm/utils/ntp.js +27 -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 +30 -0
- package/dist/homekit/credentials.js +100 -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 +109 -0
- package/dist/index.js +265 -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 +31 -0
- package/dist/utils/circularBuffer.js +124 -0
- package/dist/utils/config.d.ts +25 -0
- package/dist/utils/config.js +28 -0
- package/dist/utils/http.d.ts +19 -0
- package/dist/utils/http.js +148 -0
- package/dist/utils/ntp.d.ts +7 -0
- package/dist/utils/ntp.js +27 -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 +62 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const bigInt = require('big-integer');
|
|
4
|
+
const sha1 = require('js-sha1');
|
|
5
|
+
const memoize = require('lodash/memoize');
|
|
6
|
+
const util_1 = require("../utils/util");
|
|
7
|
+
// ...
|
|
8
|
+
const groups = {
|
|
9
|
+
1024: {
|
|
10
|
+
N: new bigInt('EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C' +
|
|
11
|
+
'9C256576D674DF7496EA81D3383B4813D692C6E0E0D5D8E250B98BE4' +
|
|
12
|
+
'8E495C1D6089DAD15DC7D7B46154D6B6CE8EF4AD69B15D4982559B29' +
|
|
13
|
+
'7BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA9A' +
|
|
14
|
+
'FD5138FE8376435B9FC61D2FC0EB06E3', 16),
|
|
15
|
+
g: new bigInt(2)
|
|
16
|
+
},
|
|
17
|
+
2048: {
|
|
18
|
+
N: new bigInt('AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC319294' +
|
|
19
|
+
'3DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310D' +
|
|
20
|
+
'CD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FB' +
|
|
21
|
+
'D5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF74' +
|
|
22
|
+
'7359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A' +
|
|
23
|
+
'436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D' +
|
|
24
|
+
'5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E73' +
|
|
25
|
+
'03CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB6' +
|
|
26
|
+
'94B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F' +
|
|
27
|
+
'9E4AFF73', 16),
|
|
28
|
+
g: new bigInt(2)
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
// ...
|
|
32
|
+
class SRP {
|
|
33
|
+
group;
|
|
34
|
+
N;
|
|
35
|
+
g;
|
|
36
|
+
constructor(group) {
|
|
37
|
+
this.group = group;
|
|
38
|
+
const groupConfig = groups[group];
|
|
39
|
+
if (!groupConfig) {
|
|
40
|
+
throw new Error(`SRP group ${group} is not supported`);
|
|
41
|
+
}
|
|
42
|
+
this.N = groupConfig.N;
|
|
43
|
+
this.g = groupConfig.g;
|
|
44
|
+
this.A = memoize(this.A.bind(this));
|
|
45
|
+
this.K = memoize(this.K.bind(this));
|
|
46
|
+
}
|
|
47
|
+
// ...
|
|
48
|
+
// Private.
|
|
49
|
+
u(A, B) {
|
|
50
|
+
const A_buf = (0, util_1.hexString2ArrayBuffer)(A);
|
|
51
|
+
const B_buf = (0, util_1.hexString2ArrayBuffer)(B);
|
|
52
|
+
const result = new Uint8Array(A_buf.byteLength + B_buf.byteLength);
|
|
53
|
+
result.set(A_buf);
|
|
54
|
+
result.set(B_buf, A_buf.byteLength);
|
|
55
|
+
return sha1(result);
|
|
56
|
+
}
|
|
57
|
+
k() {
|
|
58
|
+
const padded_g = '0'.repeat((this.group / 4) - 1) + this.g.toString(16);
|
|
59
|
+
const N_buf = (0, util_1.hexString2ArrayBuffer)(this.N.toString(16));
|
|
60
|
+
const g_buf = (0, util_1.hexString2ArrayBuffer)(padded_g);
|
|
61
|
+
const result = new Uint8Array(N_buf.byteLength + g_buf.byteLength);
|
|
62
|
+
result.set(N_buf);
|
|
63
|
+
result.set(g_buf, N_buf.byteLength);
|
|
64
|
+
return sha1(result);
|
|
65
|
+
}
|
|
66
|
+
x(I, P, s) {
|
|
67
|
+
const s_buf = (0, util_1.hexString2ArrayBuffer)(s.toLowerCase());
|
|
68
|
+
const I_P_buf = (0, util_1.hexString2ArrayBuffer)(sha1(I + ':' + P));
|
|
69
|
+
const result = new Uint8Array(s_buf.byteLength + I_P_buf.byteLength);
|
|
70
|
+
result.set(s_buf);
|
|
71
|
+
result.set(I_P_buf, s_buf.byteLength);
|
|
72
|
+
return sha1(result);
|
|
73
|
+
}
|
|
74
|
+
;
|
|
75
|
+
S(B, k, x, a, u) {
|
|
76
|
+
const Bn = new bigInt(B, 16);
|
|
77
|
+
const kn = new bigInt(k, 16);
|
|
78
|
+
const xn = new bigInt(x, 16);
|
|
79
|
+
const an = new bigInt(a, 16);
|
|
80
|
+
const un = new bigInt(u, 16);
|
|
81
|
+
return Bn.add(this.N.multiply(kn)).subtract(this.g.modPow(xn, this.N).multiply(kn)).mod(this.N)
|
|
82
|
+
.modPow(an.add(un.multiply(xn)), this.N)
|
|
83
|
+
.toString(16);
|
|
84
|
+
}
|
|
85
|
+
// ...
|
|
86
|
+
// Public.
|
|
87
|
+
A(a) {
|
|
88
|
+
return this.g.modPow(new bigInt(a, 16), this.N).toString(16);
|
|
89
|
+
}
|
|
90
|
+
K(I, P, s, a, B) {
|
|
91
|
+
const k = this.k();
|
|
92
|
+
const x = this.x(I, P, s);
|
|
93
|
+
const u = this.u(this.A(a), B);
|
|
94
|
+
const S = this.S(B, k, x, a, u);
|
|
95
|
+
// ...
|
|
96
|
+
const S_buf = (0, util_1.hexString2ArrayBuffer)(S);
|
|
97
|
+
let hash1 = new Uint8Array(S_buf.byteLength + 4);
|
|
98
|
+
hash1.set(S_buf);
|
|
99
|
+
hash1.set([0x00, 0x00, 0x00, 0x00], S_buf.byteLength);
|
|
100
|
+
let hash2 = new Uint8Array(S_buf.byteLength + 4);
|
|
101
|
+
hash2.set(S_buf);
|
|
102
|
+
hash2.set([0x00, 0x00, 0x00, 0x01], S_buf.byteLength);
|
|
103
|
+
return sha1(hash1) + sha1(hash2);
|
|
104
|
+
}
|
|
105
|
+
M1(I, P, s, a, B) {
|
|
106
|
+
// M1 = H( H(N) ^ H(g) | H(I) | s | PAD(A) | PAD(B) | K )
|
|
107
|
+
const A = this.A(a);
|
|
108
|
+
const K = this.K(I, P, s, a, B);
|
|
109
|
+
const hN = new Uint8Array(sha1.arrayBuffer((0, util_1.hexString2ArrayBuffer)(this.N.toString(16))));
|
|
110
|
+
const hg = new Uint8Array(sha1.arrayBuffer([this.g.toString(16)]));
|
|
111
|
+
const hN_hg = new Uint8Array(20);
|
|
112
|
+
for (let i = 0; i < 20; i++) {
|
|
113
|
+
hN_hg[i] = hN[i] ^ hg[i];
|
|
114
|
+
}
|
|
115
|
+
const hI = sha1.arrayBuffer(I);
|
|
116
|
+
// ...
|
|
117
|
+
return sha1.create()
|
|
118
|
+
.update(hN_hg)
|
|
119
|
+
.update(hI)
|
|
120
|
+
.update((0, util_1.hexString2ArrayBuffer)(s))
|
|
121
|
+
.update((0, util_1.hexString2ArrayBuffer)(A))
|
|
122
|
+
.update((0, util_1.hexString2ArrayBuffer)(B))
|
|
123
|
+
.update((0, util_1.hexString2ArrayBuffer)(K))
|
|
124
|
+
.hex();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ...
|
|
128
|
+
exports.default = SRP;
|
|
@@ -0,0 +1,149 @@
|
|
|
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_dgram_1 = __importDefault(require("node:dgram"));
|
|
7
|
+
const node_events_1 = require("node:events");
|
|
8
|
+
const async_1 = __importDefault(require("async"));
|
|
9
|
+
const config_1 = __importDefault(require("../utils/config"));
|
|
10
|
+
const numUtil_1 = require("../utils/numUtil");
|
|
11
|
+
const ntp_1 = __importDefault(require("../utils/ntp"));
|
|
12
|
+
const UNBOUND = 0;
|
|
13
|
+
const BINDING = 1;
|
|
14
|
+
const BOUND = 2;
|
|
15
|
+
/**
|
|
16
|
+
* Manages control/timing UDP sockets used by RAOP for resend requests and clock sync.
|
|
17
|
+
* Binds ports for both endpoints and emits events with socket info.
|
|
18
|
+
*/
|
|
19
|
+
class UDPServers extends node_events_1.EventEmitter {
|
|
20
|
+
status = UNBOUND;
|
|
21
|
+
control = { socket: null, port: null, name: 'control' };
|
|
22
|
+
timing = { socket: null, port: null, name: 'timing' };
|
|
23
|
+
hosts = [];
|
|
24
|
+
/**
|
|
25
|
+
* Bind control + timing sockets for a host and emit `ports` when ready.
|
|
26
|
+
*/
|
|
27
|
+
bind(host) {
|
|
28
|
+
this.hosts.push(host);
|
|
29
|
+
if (this.status === BOUND) {
|
|
30
|
+
process.nextTick(() => {
|
|
31
|
+
this.emit('ports', null, this.control, this.timing);
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (this.status === BINDING) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.status = BINDING;
|
|
39
|
+
this.timing.socket = node_dgram_1.default.createSocket({ type: 'udp4', reuseAddr: true });
|
|
40
|
+
this.timing.socket.on('message', (msg, rinfo) => {
|
|
41
|
+
if (!this.hosts.includes(rinfo.address))
|
|
42
|
+
return;
|
|
43
|
+
const ts1 = msg.readUInt32BE(24);
|
|
44
|
+
const ts2 = msg.readUInt32BE(28);
|
|
45
|
+
const reply = Buffer.alloc(32);
|
|
46
|
+
reply.writeUInt16BE(0x80d3, 0);
|
|
47
|
+
reply.writeUInt16BE(0x0007, 2);
|
|
48
|
+
reply.writeUInt32BE(0x00000000, 4);
|
|
49
|
+
reply.writeUInt32BE(ts1, 8);
|
|
50
|
+
reply.writeUInt32BE(ts2, 12);
|
|
51
|
+
const ntpTime = ntp_1.default.timestamp();
|
|
52
|
+
ntpTime.copy(reply, 16);
|
|
53
|
+
ntpTime.copy(reply, 24);
|
|
54
|
+
this.timing.socket?.send(reply, 0, reply.length, rinfo.port, rinfo.address);
|
|
55
|
+
});
|
|
56
|
+
this.control.socket = node_dgram_1.default.createSocket({ type: 'udp4', reuseAddr: true });
|
|
57
|
+
this.control.socket.on('message', (msg, rinfo) => {
|
|
58
|
+
if (!this.hosts.includes(rinfo.address))
|
|
59
|
+
return;
|
|
60
|
+
const resendRequested = msg.readUInt8(1) === (0x80 | 0x55);
|
|
61
|
+
if (resendRequested) {
|
|
62
|
+
const missedSeq = msg.readUInt16BE(4);
|
|
63
|
+
const count = msg.readUInt16BE(6);
|
|
64
|
+
this.emit('resendRequested', missedSeq, count);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
if (process.platform !== 'darwin') {
|
|
68
|
+
this.control.socket.on('error', (err) => {
|
|
69
|
+
this.emit('ports', err);
|
|
70
|
+
});
|
|
71
|
+
this.timing.socket.on('error', (err) => {
|
|
72
|
+
this.emit('ports', err);
|
|
73
|
+
});
|
|
74
|
+
this.control.socket.bind(0, () => {
|
|
75
|
+
this.control.port = this.control.socket?.address().port ?? null;
|
|
76
|
+
});
|
|
77
|
+
this.timing.socket.bind(0, () => {
|
|
78
|
+
this.timing.port = this.timing.socket?.address().port ?? null;
|
|
79
|
+
});
|
|
80
|
+
const interval = setInterval(() => {
|
|
81
|
+
if (this.timing.port != null && this.control.port != null) {
|
|
82
|
+
clearInterval(interval);
|
|
83
|
+
this.status = BOUND;
|
|
84
|
+
this.emit('ports', null, this.control, this.timing);
|
|
85
|
+
}
|
|
86
|
+
}, 100);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const toBind = [this.control, this.timing];
|
|
90
|
+
let currentPort = config_1.default.udp_default_port;
|
|
91
|
+
async_1.default.whilst((cb) => cb(null, toBind.length > 0), (cb) => {
|
|
92
|
+
const nextPort = toBind[0];
|
|
93
|
+
nextPort.socket?.once('error', (err) => {
|
|
94
|
+
if (err.code === 'EADDRINUSE') {
|
|
95
|
+
currentPort += 1;
|
|
96
|
+
cb();
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
cb(err);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
nextPort.socket?.once('listening', () => {
|
|
103
|
+
toBind.shift();
|
|
104
|
+
nextPort.port = currentPort;
|
|
105
|
+
currentPort += 1;
|
|
106
|
+
cb();
|
|
107
|
+
});
|
|
108
|
+
nextPort.socket?.bind(currentPort);
|
|
109
|
+
}, (err) => {
|
|
110
|
+
if (err) {
|
|
111
|
+
this.close();
|
|
112
|
+
this.emit('ports', err);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
this.status = BOUND;
|
|
116
|
+
this.emit('ports', null, this.control, this.timing);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/** Close sockets and reset state. */
|
|
121
|
+
close() {
|
|
122
|
+
try {
|
|
123
|
+
this.status = UNBOUND;
|
|
124
|
+
this.timing.socket?.close();
|
|
125
|
+
this.timing.socket = null;
|
|
126
|
+
this.control.socket?.close();
|
|
127
|
+
this.control.socket = null;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// ignore
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Send an RTCP sync packet to a receiver to align playback.
|
|
135
|
+
*/
|
|
136
|
+
sendControlSync(seq, dev) {
|
|
137
|
+
if (this.status !== BOUND || !this.control.socket)
|
|
138
|
+
return;
|
|
139
|
+
const packet = Buffer.alloc(20);
|
|
140
|
+
packet.writeUInt16BE(0x80d4, 0);
|
|
141
|
+
packet.writeUInt16BE(0x0007, 2);
|
|
142
|
+
packet.writeUInt32BE((0, numUtil_1.low32)(seq * config_1.default.frames_per_packet), 4);
|
|
143
|
+
const ntpTime = ntp_1.default.timestamp();
|
|
144
|
+
ntpTime.copy(packet, 8);
|
|
145
|
+
packet.writeUInt32BE((0, numUtil_1.low32)(seq * config_1.default.frames_per_packet + config_1.default.sampling_rate * 2), 16);
|
|
146
|
+
this.control.socket.send(packet, 0, packet.length, dev.controlPort, dev.host);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
exports.default = UDPServers;
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
}
|
|
100
|
+
exports.Credentials = Credentials;
|
|
@@ -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,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Type Length Value encoding/decoding, used by HAP as a wire format.
|
|
5
|
+
* https://en.wikipedia.org/wiki/Type-length-value
|
|
6
|
+
*/
|
|
7
|
+
const Tag = {
|
|
8
|
+
PairingMethod: 0x00,
|
|
9
|
+
Username: 0x01,
|
|
10
|
+
Salt: 0x02,
|
|
11
|
+
// could be either the SRP client public key (384 bytes) or the ED25519 public key (32 bytes), depending on context
|
|
12
|
+
PublicKey: 0x03,
|
|
13
|
+
Proof: 0x04,
|
|
14
|
+
EncryptedData: 0x05,
|
|
15
|
+
Sequence: 0x06,
|
|
16
|
+
ErrorCode: 0x07,
|
|
17
|
+
BackOff: 0x08,
|
|
18
|
+
Signature: 0x0a,
|
|
19
|
+
MFiCertificate: 0x09,
|
|
20
|
+
MFiSignature: 0x0a,
|
|
21
|
+
Flags: 0x13,
|
|
22
|
+
};
|
|
23
|
+
function encodeOne(type, data) {
|
|
24
|
+
let bufferData;
|
|
25
|
+
if (typeof data === 'number') {
|
|
26
|
+
bufferData = Buffer.from([data]);
|
|
27
|
+
}
|
|
28
|
+
else if (typeof data === 'string') {
|
|
29
|
+
bufferData = Buffer.from(data);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
bufferData = data;
|
|
33
|
+
}
|
|
34
|
+
if (bufferData.length <= 255) {
|
|
35
|
+
return Buffer.concat([Buffer.from([type, bufferData.length]), bufferData]);
|
|
36
|
+
}
|
|
37
|
+
let leftLength = bufferData.length;
|
|
38
|
+
let tempBuffer = Buffer.alloc(0);
|
|
39
|
+
let currentStart = 0;
|
|
40
|
+
for (; leftLength > 0;) {
|
|
41
|
+
if (leftLength >= 255) {
|
|
42
|
+
tempBuffer = Buffer.concat([
|
|
43
|
+
tempBuffer,
|
|
44
|
+
Buffer.from([type, 0xff]),
|
|
45
|
+
bufferData.slice(currentStart, currentStart + 255),
|
|
46
|
+
]);
|
|
47
|
+
leftLength -= 255;
|
|
48
|
+
currentStart += 255;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
tempBuffer = Buffer.concat([
|
|
52
|
+
tempBuffer,
|
|
53
|
+
Buffer.from([type, leftLength]),
|
|
54
|
+
bufferData.slice(currentStart, currentStart + leftLength),
|
|
55
|
+
]);
|
|
56
|
+
leftLength = 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return tempBuffer;
|
|
60
|
+
}
|
|
61
|
+
function encode(type, data, ...args) {
|
|
62
|
+
const encodedTLVBuffer = encodeOne(type, data);
|
|
63
|
+
if (args.length === 0) {
|
|
64
|
+
return encodedTLVBuffer;
|
|
65
|
+
}
|
|
66
|
+
const nextType = args[0];
|
|
67
|
+
const nextData = args[1];
|
|
68
|
+
const remaining = args.slice(2);
|
|
69
|
+
const remainingTLVBuffer = encode(nextType, nextData, ...remaining);
|
|
70
|
+
return Buffer.concat([encodedTLVBuffer, remainingTLVBuffer]);
|
|
71
|
+
}
|
|
72
|
+
function decode(data) {
|
|
73
|
+
const objects = {};
|
|
74
|
+
let leftLength = data.length;
|
|
75
|
+
let currentIndex = 0;
|
|
76
|
+
for (; leftLength > 0;) {
|
|
77
|
+
const type = data[currentIndex];
|
|
78
|
+
const length = data[currentIndex + 1];
|
|
79
|
+
currentIndex += 2;
|
|
80
|
+
leftLength -= 2;
|
|
81
|
+
const newData = data.slice(currentIndex, currentIndex + length);
|
|
82
|
+
if (objects[type]) {
|
|
83
|
+
objects[type] = Buffer.concat([objects[type], newData]);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
objects[type] = newData;
|
|
87
|
+
}
|
|
88
|
+
currentIndex += length;
|
|
89
|
+
leftLength -= length;
|
|
90
|
+
}
|
|
91
|
+
return objects;
|
|
92
|
+
}
|
|
93
|
+
exports.default = {
|
|
94
|
+
Tag,
|
|
95
|
+
encode,
|
|
96
|
+
decode,
|
|
97
|
+
};
|