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,14 @@
|
|
|
1
|
+
declare class SRP {
|
|
2
|
+
group: number;
|
|
3
|
+
N: any;
|
|
4
|
+
g: any;
|
|
5
|
+
constructor(group: number);
|
|
6
|
+
u(A: string, B: string): any;
|
|
7
|
+
k(): string;
|
|
8
|
+
x(I: string, P: string, s: string): string;
|
|
9
|
+
S(B: string, k: string, x: string, a: string, u: string): string;
|
|
10
|
+
A(a: string): string;
|
|
11
|
+
K(I: string, P: string, s: string, a: string, B: string): string;
|
|
12
|
+
M1(I: string, P: string, s: string, a: string, B: string): string;
|
|
13
|
+
}
|
|
14
|
+
export default SRP;
|
package/dist/core/srp.js
ADDED
|
@@ -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,26 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
type ControlSyncTarget = {
|
|
3
|
+
host: string;
|
|
4
|
+
controlPort: number;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Manages control/timing UDP sockets used by RAOP for resend requests and clock sync.
|
|
8
|
+
* Binds ports for both endpoints and emits events with socket info.
|
|
9
|
+
*/
|
|
10
|
+
export default class UDPServers extends EventEmitter {
|
|
11
|
+
private status;
|
|
12
|
+
private readonly control;
|
|
13
|
+
private readonly timing;
|
|
14
|
+
private readonly hosts;
|
|
15
|
+
/**
|
|
16
|
+
* Bind control + timing sockets for a host and emit `ports` when ready.
|
|
17
|
+
*/
|
|
18
|
+
bind(host: string): void;
|
|
19
|
+
/** Close sockets and reset state. */
|
|
20
|
+
close(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Send an RTCP sync packet to a receiver to align playback.
|
|
23
|
+
*/
|
|
24
|
+
sendControlSync(seq: number, dev: ControlSyncTarget): void;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -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,8 @@
|
|
|
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 number_1 = __importDefault(require("../homekit/number"));
|
|
7
|
+
const u = number_1.default.UInt16toBufferBE(287);
|
|
8
|
+
console.log(Buffer.concat([u.slice(1), u.slice(0, 1)]));
|
|
@@ -0,0 +1,215 @@
|
|
|
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
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const bplist_creator_1 = __importDefault(require("bplist-creator"));
|
|
9
|
+
const bplist_parser_1 = __importDefault(require("bplist-parser"));
|
|
10
|
+
const srp_1 = __importDefault(require("./srp"));
|
|
11
|
+
const atvAuthenticator_1 = __importDefault(require("./atvAuthenticator"));
|
|
12
|
+
const http_1 = __importDefault(require("../utils/http"));
|
|
13
|
+
// ...
|
|
14
|
+
// Configuration.
|
|
15
|
+
const loadConfig = (configFilePath) => !fs_1.default.existsSync(configFilePath) ? null : JSON.parse(fs_1.default.readFileSync(configFilePath, 'utf8'));
|
|
16
|
+
const saveConfig = (configFilePath, config) => fs_1.default.writeFileSync(configFilePath, JSON.stringify(config, null, '\t'));
|
|
17
|
+
// ...
|
|
18
|
+
class ATV {
|
|
19
|
+
addr;
|
|
20
|
+
port;
|
|
21
|
+
httpClient;
|
|
22
|
+
auth_secret;
|
|
23
|
+
constructor(addr, port) {
|
|
24
|
+
this.addr = addr;
|
|
25
|
+
this.port = port || 7000;
|
|
26
|
+
this.httpClient = (0, http_1.default)();
|
|
27
|
+
this.auth_secret = null;
|
|
28
|
+
}
|
|
29
|
+
// ...
|
|
30
|
+
auth(configFilePath, authenticator) {
|
|
31
|
+
async function auth(owner) {
|
|
32
|
+
await owner.httpClient.connect(owner.addr, owner.port);
|
|
33
|
+
let conf = loadConfig(configFilePath);
|
|
34
|
+
const authSecret = conf && typeof conf['auth_secret'] === 'string' ? conf['auth_secret'] : null;
|
|
35
|
+
if (!authSecret) {
|
|
36
|
+
// a pairing does not exist and must be performed.
|
|
37
|
+
// ...
|
|
38
|
+
// SRP parameters.
|
|
39
|
+
const srp = new srp_1.default(2048);
|
|
40
|
+
const I = '366B4165DD64AD3A';
|
|
41
|
+
let P;
|
|
42
|
+
let s;
|
|
43
|
+
let B;
|
|
44
|
+
let a;
|
|
45
|
+
let A;
|
|
46
|
+
let M1;
|
|
47
|
+
await owner.httpClient.request('POST', '/pair-pin-start')
|
|
48
|
+
.then(() => authenticator())
|
|
49
|
+
.then(pin => {
|
|
50
|
+
P = pin;
|
|
51
|
+
return owner.httpClient.request('POST', '/pair-setup-pin', {
|
|
52
|
+
'Content-Type': 'application/x-apple-binary-plist'
|
|
53
|
+
}, (0, bplist_creator_1.default)({
|
|
54
|
+
user: '366B4165DD64AD3A',
|
|
55
|
+
method: 'pin'
|
|
56
|
+
}));
|
|
57
|
+
})
|
|
58
|
+
.then((res) => {
|
|
59
|
+
const { pk, salt } = bplist_parser_1.default.parseBuffer(res.body)[0];
|
|
60
|
+
s = salt.toString('hex');
|
|
61
|
+
B = pk.toString('hex');
|
|
62
|
+
// SRP: Generate random auth_secret, 'a'; if pairing is successful, it'll be utilized in
|
|
63
|
+
// subsequent session authentication(s).
|
|
64
|
+
a = crypto_1.default.randomBytes(32).toString('hex');
|
|
65
|
+
// SRP: Compute A and M1.
|
|
66
|
+
A = srp.A(a);
|
|
67
|
+
M1 = srp.M1(I, P, s, a, B);
|
|
68
|
+
return owner.httpClient.request('POST', '/pair-setup-pin', {
|
|
69
|
+
'Content-Type': 'application/x-apple-binary-plist'
|
|
70
|
+
}, (0, bplist_creator_1.default)({
|
|
71
|
+
pk: Buffer.from(A, 'hex'),
|
|
72
|
+
proof: Buffer.from(M1, 'hex')
|
|
73
|
+
}));
|
|
74
|
+
})
|
|
75
|
+
.then(() => {
|
|
76
|
+
// confirm the auth secret (a).
|
|
77
|
+
const { epk, authTag } = atvAuthenticator_1.default.confirm(a, srp.K(I, P, s, a, B));
|
|
78
|
+
// complete pair-setup-pin by registering the auth secret with the target device.
|
|
79
|
+
return owner.httpClient.request('POST', '/pair-setup-pin', {
|
|
80
|
+
'Content-Type': 'application/x-apple-binary-plist'
|
|
81
|
+
}, (0, bplist_creator_1.default)({
|
|
82
|
+
epk: Buffer.from(epk, 'hex'),
|
|
83
|
+
authTag: Buffer.from(authTag, 'hex')
|
|
84
|
+
}));
|
|
85
|
+
})
|
|
86
|
+
.then(() => {
|
|
87
|
+
// save the auth secret for subsequent session authentication(s).
|
|
88
|
+
if (!conf) {
|
|
89
|
+
conf = {};
|
|
90
|
+
}
|
|
91
|
+
conf['auth_secret'] = a;
|
|
92
|
+
saveConfig(configFilePath, conf);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// ...
|
|
96
|
+
// Authenticate session with the target device using existing pairing information.
|
|
97
|
+
const verifier = atvAuthenticator_1.default.verifier(conf?.['auth_secret'] ?? '');
|
|
98
|
+
return owner.httpClient.request('POST', '/pair-verify', {
|
|
99
|
+
'Content-Type': 'application/octet-stream'
|
|
100
|
+
}, verifier.verifierBody)
|
|
101
|
+
.then((res) => {
|
|
102
|
+
const atv_pub = res.body.slice(0, 32).toString('hex');
|
|
103
|
+
const atv_data = res.body.slice(32).toString('hex');
|
|
104
|
+
const shared = atvAuthenticator_1.default.shared(verifier.v_pri, atv_pub);
|
|
105
|
+
const signed = atvAuthenticator_1.default.signed(conf?.['auth_secret'] ?? '', verifier.v_pub, atv_pub);
|
|
106
|
+
const signature = Buffer.from(Buffer.from([0x00, 0x00, 0x00, 0x00]).toString('hex') +
|
|
107
|
+
atvAuthenticator_1.default.signature(shared, atv_data, signed), 'hex');
|
|
108
|
+
return owner.httpClient.request('POST', '/pair-verify', {
|
|
109
|
+
'Content-Type': 'application/octet-stream'
|
|
110
|
+
}, signature);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return auth(this);
|
|
114
|
+
}
|
|
115
|
+
authSecret() {
|
|
116
|
+
return this.auth_secret;
|
|
117
|
+
}
|
|
118
|
+
authSimple(authenticator) {
|
|
119
|
+
async function auth(owner) {
|
|
120
|
+
await owner.httpClient.connect(owner.addr, owner.port);
|
|
121
|
+
const conf = null;
|
|
122
|
+
if (!conf || !conf['auth_secret']) {
|
|
123
|
+
// a pairing does not exist and must be performed.
|
|
124
|
+
// ...
|
|
125
|
+
// SRP parameters.
|
|
126
|
+
const srp = new srp_1.default(2048);
|
|
127
|
+
const I = '366B4165DD64AD3A';
|
|
128
|
+
let P;
|
|
129
|
+
let s;
|
|
130
|
+
let B;
|
|
131
|
+
let a;
|
|
132
|
+
let A;
|
|
133
|
+
let M1;
|
|
134
|
+
await owner.httpClient.request('POST', '/pair-pin-start')
|
|
135
|
+
.then(() => authenticator())
|
|
136
|
+
.then(pin => {
|
|
137
|
+
P = pin;
|
|
138
|
+
return owner.httpClient.request('POST', '/pair-setup-pin', {
|
|
139
|
+
'Content-Type': 'application/x-apple-binary-plist'
|
|
140
|
+
}, (0, bplist_creator_1.default)({
|
|
141
|
+
user: '366B4165DD64AD3A',
|
|
142
|
+
method: 'pin'
|
|
143
|
+
}));
|
|
144
|
+
})
|
|
145
|
+
.then((res) => {
|
|
146
|
+
const { pk, salt } = bplist_parser_1.default.parseBuffer(res.body)[0];
|
|
147
|
+
s = salt.toString('hex');
|
|
148
|
+
B = pk.toString('hex');
|
|
149
|
+
// SRP: Generate random auth_secret, 'a'; if pairing is successful, it'll be utilized in
|
|
150
|
+
// subsequent session authentication(s).
|
|
151
|
+
a = crypto_1.default.randomBytes(32).toString('hex');
|
|
152
|
+
// SRP: Compute A and M1.
|
|
153
|
+
A = srp.A(a);
|
|
154
|
+
M1 = srp.M1(I, P, s, a, B);
|
|
155
|
+
return owner.httpClient.request('POST', '/pair-setup-pin', {
|
|
156
|
+
'Content-Type': 'application/x-apple-binary-plist'
|
|
157
|
+
}, (0, bplist_creator_1.default)({
|
|
158
|
+
pk: Buffer.from(A, 'hex'),
|
|
159
|
+
proof: Buffer.from(M1, 'hex')
|
|
160
|
+
}));
|
|
161
|
+
}).then(() => {
|
|
162
|
+
// confirm the auth secret (a).
|
|
163
|
+
const { epk, authTag } = atvAuthenticator_1.default.confirm(a, srp.K(I, P, s, a, B));
|
|
164
|
+
// complete pair-setup-pin by registering the auth secret with the target device.
|
|
165
|
+
return owner.httpClient.request('POST', '/pair-setup-pin', {
|
|
166
|
+
'Content-Type': 'application/x-apple-binary-plist'
|
|
167
|
+
}, (0, bplist_creator_1.default)({
|
|
168
|
+
epk: Buffer.from(epk, 'hex'),
|
|
169
|
+
authTag: Buffer.from(authTag, 'hex')
|
|
170
|
+
}));
|
|
171
|
+
})
|
|
172
|
+
.then(() => {
|
|
173
|
+
// save the auth secret for subsequent session authentication(s).
|
|
174
|
+
owner.auth_secret = a;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return auth(this);
|
|
179
|
+
}
|
|
180
|
+
verifySimple(secret) {
|
|
181
|
+
// ...
|
|
182
|
+
// Authenticate session with the target device using existing pairing information.
|
|
183
|
+
const verifier = atvAuthenticator_1.default.verifier(secret);
|
|
184
|
+
return this.httpClient.request('POST', '/pair-verify', {
|
|
185
|
+
'Content-Type': 'application/octet-stream',
|
|
186
|
+
}, verifier.verifierBody)
|
|
187
|
+
.then((res) => {
|
|
188
|
+
const atv_pub = res.body.slice(0, 32).toString('hex');
|
|
189
|
+
const atv_data = res.body.slice(32).toString('hex');
|
|
190
|
+
const shared = atvAuthenticator_1.default.shared(verifier.v_pri, atv_pub);
|
|
191
|
+
const signed = atvAuthenticator_1.default.signed(secret, verifier.v_pub, atv_pub);
|
|
192
|
+
const signature = Buffer.from(Buffer.from([0x00, 0x00, 0x00, 0x00]).toString('hex') +
|
|
193
|
+
atvAuthenticator_1.default.signature(shared, atv_data, signed), 'hex');
|
|
194
|
+
return this.httpClient.request('POST', '/pair-verify', {
|
|
195
|
+
'Content-Type': 'application/octet-stream',
|
|
196
|
+
}, signature);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
play(videoUrl) {
|
|
200
|
+
return this.httpClient.request('POST', '/play', {
|
|
201
|
+
'Content-Type': 'application/x-apple-binary-plist'
|
|
202
|
+
}, (0, bplist_creator_1.default)({
|
|
203
|
+
'Content-Location': videoUrl,
|
|
204
|
+
'Start-Location': 0
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
stop() {
|
|
208
|
+
return this.httpClient.request('POST', '/stop');
|
|
209
|
+
}
|
|
210
|
+
close() {
|
|
211
|
+
this.httpClient.close();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// ...
|
|
215
|
+
exports.default = ATV;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const elliptic = require('elliptic');
|
|
40
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
41
|
+
const ed = __importStar(require("@noble/ed25519"));
|
|
42
|
+
const util_1 = require("../utils/util");
|
|
43
|
+
// ...
|
|
44
|
+
// Note: All functions expect parameters to be hex strings.
|
|
45
|
+
function pair_setup_aes_key(K) {
|
|
46
|
+
return crypto_1.default.createHash('sha512')
|
|
47
|
+
.update('Pair-Setup-AES-Key')
|
|
48
|
+
.update((0, util_1.hexString2ArrayBuffer)(K))
|
|
49
|
+
.digest('hex')
|
|
50
|
+
.substring(0, 32);
|
|
51
|
+
}
|
|
52
|
+
function pair_setup_aes_iv(K) {
|
|
53
|
+
let ab = crypto_1.default.createHash('sha512')
|
|
54
|
+
.update('Pair-Setup-AES-IV')
|
|
55
|
+
.update((0, util_1.hexString2ArrayBuffer)(K))
|
|
56
|
+
.digest();
|
|
57
|
+
ab = ab.slice(0, 16);
|
|
58
|
+
ab[ab.length - 1] += 0x01;
|
|
59
|
+
return (0, util_1.buf2hex)(ab);
|
|
60
|
+
}
|
|
61
|
+
function pair_verify_aes_key(shared) {
|
|
62
|
+
return (0, util_1.buf2hex)(crypto_1.default.createHash('sha512')
|
|
63
|
+
.update('Pair-Verify-AES-Key')
|
|
64
|
+
.update((0, util_1.hexString2ArrayBuffer)(shared))
|
|
65
|
+
.digest()
|
|
66
|
+
.slice(0, 16));
|
|
67
|
+
}
|
|
68
|
+
function pair_verify_aes_iv(shared) {
|
|
69
|
+
return (0, util_1.buf2hex)(crypto_1.default.createHash('sha512')
|
|
70
|
+
.update('Pair-Verify-AES-IV')
|
|
71
|
+
.update((0, util_1.hexString2ArrayBuffer)(shared))
|
|
72
|
+
.digest()
|
|
73
|
+
.slice(0, 16));
|
|
74
|
+
}
|
|
75
|
+
// ...
|
|
76
|
+
// Public.
|
|
77
|
+
function a_pub(a) {
|
|
78
|
+
return elliptic.utils.toHex(new elliptic.eddsa('ed25519').keyFromSecret(a).getPublic());
|
|
79
|
+
}
|
|
80
|
+
function confirm(a, K) {
|
|
81
|
+
const key = pair_setup_aes_key(K);
|
|
82
|
+
const iv = pair_setup_aes_iv(K);
|
|
83
|
+
const cipher = crypto_1.default.createCipheriv('aes-128-gcm', (0, util_1.hexString2ArrayBuffer)(key), (0, util_1.hexString2ArrayBuffer)(iv));
|
|
84
|
+
const encrypted = Buffer.concat([
|
|
85
|
+
cipher.update((0, util_1.hexString2ArrayBuffer)(a_pub(a))),
|
|
86
|
+
cipher.final(),
|
|
87
|
+
]);
|
|
88
|
+
return {
|
|
89
|
+
epk: encrypted.toString('hex'),
|
|
90
|
+
authTag: (0, util_1.buf2hex)(cipher.getAuthTag()),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function verifier(a) {
|
|
94
|
+
const privateKey = Buffer.from(ed.utils.randomPrivateKey());
|
|
95
|
+
const publicKey = Buffer.from(ed.curve25519.scalarMultBase(privateKey));
|
|
96
|
+
const v_pri = (0, util_1.buf2hex)(privateKey);
|
|
97
|
+
const v_pub = (0, util_1.buf2hex)(publicKey);
|
|
98
|
+
const header = Buffer.from([0x01, 0x00, 0x00, 0x00]);
|
|
99
|
+
const a_pub_buf = Buffer.from(a_pub(a), 'hex');
|
|
100
|
+
return {
|
|
101
|
+
verifierBody: Buffer.concat([header, publicKey, a_pub_buf], header.byteLength + publicKey.byteLength + a_pub_buf.byteLength),
|
|
102
|
+
v_pri,
|
|
103
|
+
v_pub
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function shared(v_pri, atv_pub) {
|
|
107
|
+
return (0, util_1.buf2hex)(Buffer.from(ed.curve25519.scalarMult((0, util_1.hexString2ArrayBuffer)(v_pri), (0, util_1.hexString2ArrayBuffer)(atv_pub))));
|
|
108
|
+
}
|
|
109
|
+
function signed(a, v_pub, atv_pub) {
|
|
110
|
+
const key = new elliptic.eddsa('ed25519').keyFromSecret(a);
|
|
111
|
+
return key.sign(v_pub + atv_pub).toHex();
|
|
112
|
+
}
|
|
113
|
+
function signature(shared, atv_data, signed) {
|
|
114
|
+
const cipher = crypto_1.default.createCipheriv('aes-128-ctr', (0, util_1.hexString2ArrayBuffer)(pair_verify_aes_key(shared)), (0, util_1.hexString2ArrayBuffer)(pair_verify_aes_iv(shared)));
|
|
115
|
+
// discard the result of encrypting atv_data.
|
|
116
|
+
cipher.update((0, util_1.hexString2ArrayBuffer)(atv_data));
|
|
117
|
+
const encrypted = Buffer.concat([
|
|
118
|
+
cipher.update(Buffer.from(signed, 'hex')),
|
|
119
|
+
cipher.final(),
|
|
120
|
+
]);
|
|
121
|
+
return encrypted.toString('hex');
|
|
122
|
+
}
|
|
123
|
+
exports.default = {
|
|
124
|
+
pair_setup_aes_key,
|
|
125
|
+
pair_setup_aes_iv,
|
|
126
|
+
pair_verify_aes_key,
|
|
127
|
+
pair_verify_aes_iv,
|
|
128
|
+
a_pub,
|
|
129
|
+
confirm,
|
|
130
|
+
verifier,
|
|
131
|
+
shared,
|
|
132
|
+
signed,
|
|
133
|
+
signature,
|
|
134
|
+
};
|