@ciphore/radiocli 0.2.4 → 0.2.6
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/CHANGELOG.md +103 -2
- package/CONTRIBUTING.md +28 -0
- package/README.md +108 -253
- package/SECURITY.md +23 -1
- package/dist/agent/alarm-service.js +10 -7
- package/dist/agent/cli.js +16 -1
- package/dist/agent/command-schema.js +47 -0
- package/dist/agent/headless-host.js +226 -81
- package/dist/agent/launcher.js +2 -1
- package/dist/agent/mcp-server.js +10 -5
- package/dist/agent/service.js +75 -40
- package/dist/agent/session.js +214 -24
- package/dist/agent/tui-session.js +213 -0
- package/dist/alarms/cli.js +19 -3
- package/dist/alarms/occurrence-cache.js +25 -0
- package/dist/alarms/runner.js +87 -23
- package/dist/alarms/schedule.js +29 -1
- package/dist/alarms/scheduler.js +11 -3
- package/dist/alarms/system-volume-linux.js +160 -0
- package/dist/alarms/system-volume-ownership.js +76 -8
- package/dist/alarms/system-volume.js +44 -23
- package/dist/cli/arguments.js +212 -0
- package/dist/cli/completion.js +44 -0
- package/dist/cli/doctor.js +156 -0
- package/dist/cli/interactive.js +79 -0
- package/dist/cli/listener.js +227 -0
- package/dist/cli/output.js +121 -0
- package/dist/cli/render-tui.js +34 -0
- package/dist/cli.js +182 -199
- package/dist/platform/capabilities.js +8 -1
- package/dist/platform/desktop-media.js +61 -0
- package/dist/platform/diagnostics.js +273 -0
- package/dist/platform/json-worker.js +111 -0
- package/dist/platform/launch-command.js +9 -6
- package/dist/platform/mpris.js +356 -0
- package/dist/platform/network.js +69 -1
- package/dist/platform/omarchy.js +147 -0
- package/dist/platform/packages.js +7 -1
- package/dist/platform/runtime.js +1 -1
- package/dist/platform/terminals.js +24 -4
- package/dist/player/airplay-discovery.js +144 -61
- package/dist/player/airplay-policy.js +15 -0
- package/dist/player/airplay-process.js +63 -0
- package/dist/player/airplay-readiness.js +42 -0
- package/dist/player/airplay-sender-health.js +49 -49
- package/dist/player/airplay-sender-package.js +5 -0
- package/dist/player/airplay-sender-patch.js +102 -4
- package/dist/player/airplay-setup.js +51 -0
- package/dist/player/airplay-worker.js +97 -41
- package/dist/player/backend-install.js +9 -6
- package/dist/player/ffmpeg-input.js +37 -0
- package/dist/player/mpv-desktop-media.js +194 -0
- package/dist/player/mpv-ipc-client.js +51 -2
- package/dist/player/player-controller.js +390 -73
- package/dist/player/radio-engine.js +282 -0
- package/dist/playlists/playlist.js +18 -16
- package/dist/providers/cache.js +291 -75
- package/dist/providers/discovery-session.js +111 -0
- package/dist/providers/provider-manager.js +106 -28
- package/dist/providers/radio-browser.js +272 -41
- package/dist/providers/radio-garden.js +4 -2
- package/dist/setup-playback.js +152 -0
- package/dist/setup.js +88 -21
- package/dist/storage/file-lock.js +130 -0
- package/dist/storage/read-settings.js +33 -0
- package/dist/storage/replace-file.js +20 -0
- package/dist/storage/repository.js +247 -0
- package/dist/storage/station-identity.js +21 -0
- package/dist/storage/station-schema.js +27 -0
- package/dist/storage/store.js +314 -76
- package/dist/types.js +22 -1
- package/dist/ui/AdaptiveContent.js +21 -24
- package/dist/ui/App.js +708 -454
- package/dist/ui/AppContent.js +5 -5
- package/dist/ui/airplay-settings.js +12 -5
- package/dist/ui/bidi-ranges.js +45 -0
- package/dist/ui/components/AdaptiveMarquee.js +16 -9
- package/dist/ui/components/ReceiverVisualRow.js +39 -0
- package/dist/ui/format.js +37 -4
- package/dist/ui/help-content.js +5 -2
- package/dist/ui/rtl-output.js +137 -0
- package/dist/ui/screen-items.js +10 -4
- package/dist/ui/screens/AirPlaySettingsScreen.js +3 -3
- package/dist/ui/screens/AlarmsScreen.js +5 -2
- package/dist/ui/screens/HelpScreen.js +3 -3
- package/dist/ui/screens/NowPlayingScreen.js +2 -21
- package/dist/ui/screens/SettingsScreen.js +16 -16
- package/dist/ui/screens/StatsScreen.js +1 -1
- package/dist/ui/text-direction.js +9 -0
- package/dist/ui/theme.js +46 -0
- package/dist/ui/use-alarm-tui.js +24 -24
- package/dist/ui/use-app-input.js +42 -8
- package/dist/ui/use-command-executor.js +21 -14
- package/dist/ui/visualizers/boat-lake-kernel.js +325 -0
- package/dist/ui/visualizers/boat-lake.js +161 -0
- package/dist/ui/visualizers/gallop.js +2 -2
- package/dist/ui/visualizers/receiver-style-registry.js +3 -1
- package/dist/ui/visualizers/receiver-visualizers.js +6 -108
- package/dist/ui/visualizers/sumi-ocean.js +183 -0
- package/dist/ui/visualizers/terminal-cell.js +45 -0
- package/dist/update-check.js +2 -1
- package/docs/THIRD_PARTY_NOTICES.md +6 -4
- package/docs/boat-lake.md +88 -0
- package/docs/sumi-ocean.md +80 -0
- package/package.json +36 -12
- package/scripts/build-airplay-native.mjs +22 -0
- package/vendor/node-airtunes2/LICENSE +208 -0
- package/vendor/node-airtunes2/RADIOCLI_PATCH.md +29 -0
- package/vendor/node-airtunes2/README.md +225 -0
- package/vendor/node-airtunes2/UPSTREAM.json +50 -0
- package/vendor/node-airtunes2/binding.gyp +41 -0
- package/vendor/node-airtunes2/lib/ap2_test.js +4 -0
- package/vendor/node-airtunes2/lib/atv.js +307 -0
- package/vendor/node-airtunes2/lib/atvAuthenticator.js +136 -0
- package/vendor/node-airtunes2/lib/audio_out.js +69 -0
- package/vendor/node-airtunes2/lib/circular_buffer.js +133 -0
- package/vendor/node-airtunes2/lib/config.js +26 -0
- package/vendor/node-airtunes2/lib/device_airtunes.js +532 -0
- package/vendor/node-airtunes2/lib/devices.js +266 -0
- package/vendor/node-airtunes2/lib/ed25519.js +26 -0
- package/vendor/node-airtunes2/lib/homekit/credentials.js +86 -0
- package/vendor/node-airtunes2/lib/homekit/encryption.js +69 -0
- package/vendor/node-airtunes2/lib/homekit/number.js +44 -0
- package/vendor/node-airtunes2/lib/homekit/tlv.js +95 -0
- package/vendor/node-airtunes2/lib/http.js +189 -0
- package/vendor/node-airtunes2/lib/index.d.ts +141 -0
- package/vendor/node-airtunes2/lib/index.js +127 -0
- package/vendor/node-airtunes2/lib/ntp.js +37 -0
- package/vendor/node-airtunes2/lib/num_util.js +21 -0
- package/vendor/node-airtunes2/lib/packet_pool.js +43 -0
- package/vendor/node-airtunes2/lib/rtsp.js +1807 -0
- package/vendor/node-airtunes2/lib/srp.js +158 -0
- package/vendor/node-airtunes2/lib/udp_servers.js +214 -0
- package/vendor/node-airtunes2/lib/util.js +12 -0
- package/vendor/node-airtunes2/package.json +31 -0
- package/vendor/node-airtunes2/src/CADebugMacros.h +257 -0
- package/vendor/node-airtunes2/src/CAHostTimeBase.cpp +105 -0
- package/vendor/node-airtunes2/src/CAHostTimeBase.h +224 -0
- package/vendor/node-airtunes2/src/aes_utils.c +864 -0
- package/vendor/node-airtunes2/src/aes_utils.h +24 -0
- package/vendor/node-airtunes2/src/base64.c +136 -0
- package/vendor/node-airtunes2/src/base64.h +42 -0
- package/vendor/node-airtunes2/src/bindings.cc +29 -0
- package/vendor/node-airtunes2/src/codec.cc +170 -0
- package/vendor/node-airtunes2/src/coreaudio.cc +332 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"url": "https://registry.npmjs.org/node-airtunes2/-/node-airtunes2-2.5.0.tgz",
|
|
3
|
+
"integrity": "sha512-OMbxDCTwakRwTEZ/PKA6HJOOHD1FiqYnw98RtGx/X3lFsNXyKPUMLyR43t9lQArRCYGsGgLuPwDv9Ehkw4mhHQ==",
|
|
4
|
+
"sha256": "2cee1d86fcd2e5fb4f56f12e23fd1abbdbd0b71a9bc5e1ffe1dcbb885cec1d71",
|
|
5
|
+
"unmodifiedFiles": {
|
|
6
|
+
".editorconfig": "4e40c0a34e2c5d50eeaeb682bb5fd3f72f79e5cbd744729d309bf3bc5f1a7742",
|
|
7
|
+
".nvmrc": "f14b4987904bcb5814e4459a057ed4d20f58a633152288a761214dcd28780b56",
|
|
8
|
+
"LICENSE": "2c0434c24a2fd80ef3c0dcca3c12a7618f3b0194b6bf9dc545695be86dc6c8e0",
|
|
9
|
+
"README.md": "230de9b984355778dcd8524725abd408bd3981ff6d419372135e6eab9a765a00",
|
|
10
|
+
"binding.gyp": "53cb9eb3fb6ccf3d35f0db61446dbe549d38a592644ba671c25b247a047f2a38",
|
|
11
|
+
"eslint.config.mjs": "b737b0a39d662188d2183fd06588821a996ddd920bce57d7fed758d52a6dd2e2",
|
|
12
|
+
"example_execlient.js": "950d83e8069032fbec39ddc6cd5d1ac8a5241dd5742e44b797f46d40fe95d3d0",
|
|
13
|
+
"lib/ap2_test.js": "9d902c2ccf7ec6ba45800ddf2db46fe5e8c2ad959d60aed2feb151c6fe4f6804",
|
|
14
|
+
"lib/atv.js": "a46235ad8b4b02a4d3b8abca64eba6d36af00e1b7d66f3669f34c65efa2f5ddd",
|
|
15
|
+
"lib/audio_out.js": "0481f988ec3e76ce33a5fe771eeb20107183850e22310325a7cb4799b72f8cc6",
|
|
16
|
+
"lib/circular_buffer.js": "e5521fc371be58bf6aa61b0bbdb906a48da4fd90c012e6db45ce4979a85c7719",
|
|
17
|
+
"lib/config.js": "18a2e5bcc6c41804945c28af3c14ed3a306e19c48aad4f4624ff30a7c9e14e49",
|
|
18
|
+
"lib/device_airtunes.js": "4782a671f6fa0924e8a48119a69b6ff93764d2265aedb6e293f284c773261a9a",
|
|
19
|
+
"lib/devices.js": "90b40223384c81f5635bad7b27bbe0d2061a2653c70053aed53ba6ecb4be6852",
|
|
20
|
+
"lib/homekit/credentials.js": "aa74c977d9831f064164ef30a8a12fd2a9eb06ce4d787fc28d8a2c57bfc2c1a7",
|
|
21
|
+
"lib/homekit/encryption.js": "e1ce0ef009309797d91b70d5f27f8234ef9fda1bb325ecb78a6fd296d87e03db",
|
|
22
|
+
"lib/homekit/number.js": "fd34215e79e61f75fefab06188ff3041982b933d4c406c7638ccb90630f1c6cf",
|
|
23
|
+
"lib/homekit/tlv.js": "447d1544d6befbe2281d50027a419cdecf4c2c9ee9e226ba216161046545e678",
|
|
24
|
+
"lib/http.js": "c7c32939c30e214c6e64de5f63eb0f3764122e988a76d85e606b72d86492effa",
|
|
25
|
+
"lib/index.d.ts": "365cc6d2375896b8141c1f68b76d3e0940ede807f34b3dcbd428e4f45674d674",
|
|
26
|
+
"lib/index.js": "b1c112d4e7b8e378b9ec9fa21cf4b5147cd773f887c99062f54a6a2afb410e63",
|
|
27
|
+
"lib/ntp.js": "208491f53fb4dc9e872eec1953d13aebf7f3c6d042a32020faf402a92008a8df",
|
|
28
|
+
"lib/num_util.js": "6a9585f05cd0fdb709dfb7eb63d554de377831e8f37425cd01028f8fa1d85f13",
|
|
29
|
+
"lib/packet_pool.js": "aef0dc8c9fe4be88a212134b2703f3e6551454e991c8e04d869f3a2cddbb2960",
|
|
30
|
+
"lib/rtsp.js": "99f5fa32e4cb29817ee57d79dd103f8f3bee34217a8032e92df3b5c8b3daa3ca",
|
|
31
|
+
"lib/srp.js": "878ca193826b216edaff1f428cd7645d85bcc0d2a8c10de4e5145454a0f0d631",
|
|
32
|
+
"lib/udp_servers.js": "fcd41ba9ba2b25db8aa74cdb3606f8a8f0201c99cb6c26bbc30a344ade36380d",
|
|
33
|
+
"lib/util.js": "6aeffbe069d052157079bc4fd508ab616de80e08b219dff2a6f30ac4c8303c65",
|
|
34
|
+
"src/CADebugMacros.h": "bc139e5ceb89790191b71ce61038f532f755707608f123a8f4783eb6f98e6872",
|
|
35
|
+
"src/CAHostTimeBase.cpp": "e3db4379731e39ae6d5699aa9625dcf286cfc9ffacafa769c87b746d663186ce",
|
|
36
|
+
"src/CAHostTimeBase.h": "27d74cbf90e7b4e74f44b5dda11b18ebed962a77cc530c6a4eb4456cdad0fda2",
|
|
37
|
+
"src/aes_utils.c": "2d72155b2d9319abc1d4db695fdabfbbdbbaad157286df75a0500653352053ad",
|
|
38
|
+
"src/aes_utils.h": "500146ef43ea263263e15e39c91330deb979db13c85bb0c809f2c1b7243f08f0",
|
|
39
|
+
"src/base64.c": "79d33c06704f52ce09062deb4c9659a98f826db43625f5f1840a1d8250f16d9d",
|
|
40
|
+
"src/base64.h": "ed283d2c2b5e6593d683a97621c8a7994e590529ec6576811da39eaa9ee02146",
|
|
41
|
+
"src/bindings.cc": "38da78b95543422006a1d7c87118ee966cc970b2c353d24d751c43d0d44e0090",
|
|
42
|
+
"src/codec.cc": "ba2f0ab6071e09410daed8aa6ee4fcec20ed23c627b4b46cd7078ee70b74f109",
|
|
43
|
+
"src/coreaudio.cc": "487fcf7e7430be3faf30a9338080cc9147014c1c64c7e6debd3bede117e2a615"
|
|
44
|
+
},
|
|
45
|
+
"excludedFiles": [
|
|
46
|
+
"build/",
|
|
47
|
+
"SECURITY.md",
|
|
48
|
+
"exe_api.md"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
'targets': [
|
|
3
|
+
{
|
|
4
|
+
'target_name': 'airtunes',
|
|
5
|
+
'sources': [
|
|
6
|
+
'src/codec.cc', 'src/bindings.cc',
|
|
7
|
+
'src/aes_utils.c', 'src/base64.c',
|
|
8
|
+
],
|
|
9
|
+
'conditions': [
|
|
10
|
+
['OS=="mac"', {
|
|
11
|
+
'include_dirs':
|
|
12
|
+
['/System/Library/Frameworks/Kernel.framework/Versions/A/Headers/sys'
|
|
13
|
+
],
|
|
14
|
+
|
|
15
|
+
'sources': [
|
|
16
|
+
],
|
|
17
|
+
'xcode_settings': {
|
|
18
|
+
'CLANG_CXX_LANGUAGE_STANDARD': 'c++20'
|
|
19
|
+
}
|
|
20
|
+
}],
|
|
21
|
+
['OS=="linux"', {
|
|
22
|
+
'sources': [
|
|
23
|
+
],
|
|
24
|
+
'cflags_cc': [
|
|
25
|
+
"-std=c++20"
|
|
26
|
+
]
|
|
27
|
+
}],
|
|
28
|
+
['OS=="win"', {
|
|
29
|
+
'include_dirs': ['C:\\Program Files\\OpenSSL-Win64\\include\\'],
|
|
30
|
+
"msbuild_settings": {
|
|
31
|
+
"ClCompile": {
|
|
32
|
+
"LanguageStandard": "stdcpp20"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
'sources': [
|
|
36
|
+
],
|
|
37
|
+
}],
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
const crypto = require('crypto')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const bplistCreator = require('bplist-creator')
|
|
4
|
+
const bplistParser = require('bplist-parser')
|
|
5
|
+
const SRP = require('./srp')
|
|
6
|
+
const ATVAuthenticator = require('./atvAuthenticator')
|
|
7
|
+
const httpClientFactory = require('./http')
|
|
8
|
+
|
|
9
|
+
// ...
|
|
10
|
+
// Configuration.
|
|
11
|
+
const loadConfig = configFilePath => !fs.existsSync(configFilePath) ? null : JSON.parse(fs.readFileSync(configFilePath))
|
|
12
|
+
const saveConfig = (configFilePath, config) => fs.writeFileSync(configFilePath, JSON.stringify(config, null, '\t'))
|
|
13
|
+
|
|
14
|
+
// ...
|
|
15
|
+
|
|
16
|
+
class ATV {
|
|
17
|
+
constructor(addr, port) {
|
|
18
|
+
this.addr = addr
|
|
19
|
+
this.port = port || 7000
|
|
20
|
+
|
|
21
|
+
this.httpClient = httpClientFactory()
|
|
22
|
+
this.auth_secret = null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ...
|
|
26
|
+
|
|
27
|
+
auth(configFilePath, authenticator) {
|
|
28
|
+
async function auth(owner) {
|
|
29
|
+
await owner.httpClient.connect(owner.addr, owner.port)
|
|
30
|
+
|
|
31
|
+
let conf = loadConfig(configFilePath)
|
|
32
|
+
if (!conf || !conf['auth_secret']) {
|
|
33
|
+
// a pairing does not exist and must be performed.
|
|
34
|
+
|
|
35
|
+
// ...
|
|
36
|
+
// SRP parameters.
|
|
37
|
+
const srp = new SRP(2048)
|
|
38
|
+
|
|
39
|
+
let I = '366B4165DD64AD3A'
|
|
40
|
+
let P
|
|
41
|
+
let s
|
|
42
|
+
let B
|
|
43
|
+
let a
|
|
44
|
+
let A
|
|
45
|
+
let M1
|
|
46
|
+
|
|
47
|
+
await owner.httpClient.request('POST', '/pair-pin-start')
|
|
48
|
+
.then(() => authenticator())
|
|
49
|
+
.then((pin) => {
|
|
50
|
+
P = pin
|
|
51
|
+
|
|
52
|
+
return owner.httpClient.request(
|
|
53
|
+
'POST',
|
|
54
|
+
'/pair-setup-pin',
|
|
55
|
+
{
|
|
56
|
+
'Content-Type': 'application/x-apple-binary-plist',
|
|
57
|
+
},
|
|
58
|
+
bplistCreator({
|
|
59
|
+
user: '366B4165DD64AD3A',
|
|
60
|
+
method: 'pin',
|
|
61
|
+
}),
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
.then((res) => {
|
|
65
|
+
const { pk, salt } = bplistParser.parseBuffer(res.body)[0]
|
|
66
|
+
|
|
67
|
+
s = salt.toString('hex')
|
|
68
|
+
B = pk.toString('hex')
|
|
69
|
+
|
|
70
|
+
// SRP: Generate random auth_secret, 'a'; if pairing is successful, it'll be utilized in
|
|
71
|
+
// subsequent session authentication(s).
|
|
72
|
+
a = crypto.randomBytes(32).toString('hex')
|
|
73
|
+
|
|
74
|
+
// SRP: Compute A and M1.
|
|
75
|
+
A = srp.A(a)
|
|
76
|
+
M1 = srp.M1(I, P, s, a, B)
|
|
77
|
+
|
|
78
|
+
return owner.httpClient.request(
|
|
79
|
+
'POST',
|
|
80
|
+
'/pair-setup-pin',
|
|
81
|
+
{
|
|
82
|
+
'Content-Type': 'application/x-apple-binary-plist',
|
|
83
|
+
},
|
|
84
|
+
bplistCreator({
|
|
85
|
+
pk: Buffer.from(A, 'hex'),
|
|
86
|
+
proof: Buffer.from(M1, 'hex'),
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
.then(() => {
|
|
91
|
+
// confirm the auth secret (a).
|
|
92
|
+
const { epk, authTag } = ATVAuthenticator.confirm(a, srp.K(I, P, s, a, B))
|
|
93
|
+
|
|
94
|
+
// complete pair-setup-pin by registering the auth secret with the target device.
|
|
95
|
+
return owner.httpClient.request(
|
|
96
|
+
'POST',
|
|
97
|
+
'/pair-setup-pin',
|
|
98
|
+
{
|
|
99
|
+
'Content-Type': 'application/x-apple-binary-plist',
|
|
100
|
+
},
|
|
101
|
+
bplistCreator({
|
|
102
|
+
epk: Buffer.from(epk, 'hex'),
|
|
103
|
+
authTag: Buffer.from(authTag, 'hex'),
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
.then(() => {
|
|
108
|
+
// save the auth secret for subsequent session authentication(s).
|
|
109
|
+
!conf && (conf = {})
|
|
110
|
+
conf['auth_secret'] = a
|
|
111
|
+
saveConfig(configFilePath, conf)
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ...
|
|
116
|
+
// Authenticate session with the target device using existing pairing information.
|
|
117
|
+
const verifier = ATVAuthenticator.verifier(conf['auth_secret'])
|
|
118
|
+
|
|
119
|
+
return owner.httpClient.request(
|
|
120
|
+
'POST',
|
|
121
|
+
'/pair-verify',
|
|
122
|
+
{
|
|
123
|
+
'Content-Type': 'application/octet-stream',
|
|
124
|
+
},
|
|
125
|
+
verifier.verifierBody,
|
|
126
|
+
)
|
|
127
|
+
.then((res) => {
|
|
128
|
+
const atv_pub = res.body.slice(0, 32).toString('hex')
|
|
129
|
+
const atv_data = res.body.slice(32).toString('hex')
|
|
130
|
+
|
|
131
|
+
const shared = ATVAuthenticator.shared(verifier.v_pri, atv_pub)
|
|
132
|
+
const signed = ATVAuthenticator.signed(conf['auth_secret'], verifier.v_pub, atv_pub)
|
|
133
|
+
const signature = Buffer.from(
|
|
134
|
+
Buffer.from([0x00, 0x00, 0x00, 0x00]).toString('hex')
|
|
135
|
+
+ ATVAuthenticator.signature(shared, atv_data, signed),
|
|
136
|
+
'hex',
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return owner.httpClient.request(
|
|
140
|
+
'POST',
|
|
141
|
+
'/pair-verify',
|
|
142
|
+
{
|
|
143
|
+
'Content-Type': 'application/octet-stream',
|
|
144
|
+
},
|
|
145
|
+
signature,
|
|
146
|
+
)
|
|
147
|
+
})
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return auth(this)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
authSecret() {
|
|
154
|
+
return this.auth_secret
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
authSimple(authenticator) {
|
|
158
|
+
async function auth(owner) {
|
|
159
|
+
await owner.httpClient.connect(owner.addr, owner.port)
|
|
160
|
+
|
|
161
|
+
let conf = null
|
|
162
|
+
if (!conf || !conf['auth_secret']) {
|
|
163
|
+
// a pairing does not exist and must be performed.
|
|
164
|
+
|
|
165
|
+
// ...
|
|
166
|
+
// SRP parameters.
|
|
167
|
+
const srp = new SRP(2048)
|
|
168
|
+
|
|
169
|
+
let I = '366B4165DD64AD3A'
|
|
170
|
+
let P
|
|
171
|
+
let s
|
|
172
|
+
let B
|
|
173
|
+
let a
|
|
174
|
+
let A
|
|
175
|
+
let M1
|
|
176
|
+
|
|
177
|
+
await owner.httpClient.request('POST', '/pair-pin-start')
|
|
178
|
+
.then(() => authenticator())
|
|
179
|
+
.then((pin) => {
|
|
180
|
+
P = pin
|
|
181
|
+
|
|
182
|
+
return owner.httpClient.request(
|
|
183
|
+
'POST',
|
|
184
|
+
'/pair-setup-pin',
|
|
185
|
+
{
|
|
186
|
+
'Content-Type': 'application/x-apple-binary-plist',
|
|
187
|
+
},
|
|
188
|
+
bplistCreator({
|
|
189
|
+
user: '366B4165DD64AD3A',
|
|
190
|
+
method: 'pin',
|
|
191
|
+
}),
|
|
192
|
+
)
|
|
193
|
+
})
|
|
194
|
+
.then((res) => {
|
|
195
|
+
const { pk, salt } = bplistParser.parseBuffer(res.body)[0]
|
|
196
|
+
|
|
197
|
+
s = salt.toString('hex')
|
|
198
|
+
B = pk.toString('hex')
|
|
199
|
+
|
|
200
|
+
// SRP: Generate random auth_secret, 'a'; if pairing is successful, it'll be utilized in
|
|
201
|
+
// subsequent session authentication(s).
|
|
202
|
+
a = crypto.randomBytes(32).toString('hex')
|
|
203
|
+
|
|
204
|
+
// SRP: Compute A and M1.
|
|
205
|
+
A = srp.A(a)
|
|
206
|
+
M1 = srp.M1(I, P, s, a, B)
|
|
207
|
+
|
|
208
|
+
return owner.httpClient.request(
|
|
209
|
+
'POST',
|
|
210
|
+
'/pair-setup-pin',
|
|
211
|
+
{
|
|
212
|
+
'Content-Type': 'application/x-apple-binary-plist',
|
|
213
|
+
},
|
|
214
|
+
bplistCreator({
|
|
215
|
+
pk: Buffer.from(A, 'hex'),
|
|
216
|
+
proof: Buffer.from(M1, 'hex'),
|
|
217
|
+
}),
|
|
218
|
+
)
|
|
219
|
+
}).then(() => {
|
|
220
|
+
// confirm the auth secret (a).
|
|
221
|
+
const { epk, authTag } = ATVAuthenticator.confirm(a, srp.K(I, P, s, a, B))
|
|
222
|
+
|
|
223
|
+
// complete pair-setup-pin by registering the auth secret with the target device.
|
|
224
|
+
return owner.httpClient.request(
|
|
225
|
+
'POST',
|
|
226
|
+
'/pair-setup-pin',
|
|
227
|
+
{
|
|
228
|
+
'Content-Type': 'application/x-apple-binary-plist',
|
|
229
|
+
},
|
|
230
|
+
bplistCreator({
|
|
231
|
+
epk: Buffer.from(epk, 'hex'),
|
|
232
|
+
authTag: Buffer.from(authTag, 'hex'),
|
|
233
|
+
}),
|
|
234
|
+
)
|
|
235
|
+
})
|
|
236
|
+
.then(() => {
|
|
237
|
+
// save the auth secret for subsequent session authentication(s).
|
|
238
|
+
console.log(a)
|
|
239
|
+
owner.auth_secret = a
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return auth(this)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
verifySimple(secret) {
|
|
247
|
+
// ...
|
|
248
|
+
// Authenticate session with the target device using existing pairing information.
|
|
249
|
+
const verifier = ATVAuthenticator.verifier(secret)
|
|
250
|
+
|
|
251
|
+
return owner.httpClient.request(
|
|
252
|
+
'POST',
|
|
253
|
+
'/pair-verify',
|
|
254
|
+
{
|
|
255
|
+
'Content-Type': 'application/octet-stream',
|
|
256
|
+
},
|
|
257
|
+
verifier.verifierBody,
|
|
258
|
+
)
|
|
259
|
+
.then((res) => {
|
|
260
|
+
const atv_pub = res.body.slice(0, 32).toString('hex')
|
|
261
|
+
const atv_data = res.body.slice(32).toString('hex')
|
|
262
|
+
|
|
263
|
+
const shared = ATVAuthenticator.shared(verifier.v_pri, atv_pub)
|
|
264
|
+
const signed = ATVAuthenticator.signed(secret, verifier.v_pub, atv_pub)
|
|
265
|
+
const signature = Buffer.from(
|
|
266
|
+
Buffer.from([0x00, 0x00, 0x00, 0x00]).toString('hex')
|
|
267
|
+
+ ATVAuthenticator.signature(shared, atv_data, signed),
|
|
268
|
+
'hex',
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return owner.httpClient.request(
|
|
272
|
+
'POST',
|
|
273
|
+
'/pair-verify',
|
|
274
|
+
{
|
|
275
|
+
'Content-Type': 'application/octet-stream',
|
|
276
|
+
},
|
|
277
|
+
signature,
|
|
278
|
+
)
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
play(videoUrl) {
|
|
283
|
+
return this.httpClient.request(
|
|
284
|
+
'POST',
|
|
285
|
+
'/play',
|
|
286
|
+
{
|
|
287
|
+
'Content-Type': 'application/x-apple-binary-plist',
|
|
288
|
+
},
|
|
289
|
+
bplistCreator({
|
|
290
|
+
'Content-Location': videoUrl,
|
|
291
|
+
'Start-Location': 0,
|
|
292
|
+
}),
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
stop() {
|
|
297
|
+
return this.httpClient.request('POST', '/stop')
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
close() {
|
|
301
|
+
this.httpClient.close()
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ...
|
|
306
|
+
|
|
307
|
+
module.exports = ATV
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// RadioCLI modification, 2026-10-16: replace elliptic with native Ed25519.
|
|
2
|
+
// Distributed under the accompanying AGPLv3; pairing formats remain unchanged.
|
|
3
|
+
const ed25519 = require('./ed25519')
|
|
4
|
+
const crypto = require('crypto')
|
|
5
|
+
const axlsign = require('axlsign')
|
|
6
|
+
const { hexString2ArrayBuffer, buf2hex } = require('./util')
|
|
7
|
+
|
|
8
|
+
// ...
|
|
9
|
+
// Note: All functions expect parameters to be hex strings.
|
|
10
|
+
|
|
11
|
+
function pair_setup_aes_key(K) {
|
|
12
|
+
return crypto.createHash('sha512')
|
|
13
|
+
.update('Pair-Setup-AES-Key')
|
|
14
|
+
.update(hexString2ArrayBuffer(K))
|
|
15
|
+
.digest('hex')
|
|
16
|
+
.substring(0, 32)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function pair_setup_aes_iv(K) {
|
|
20
|
+
let ab = crypto.createHash('sha512')
|
|
21
|
+
.update('Pair-Setup-AES-IV')
|
|
22
|
+
.update(hexString2ArrayBuffer(K))
|
|
23
|
+
.digest()
|
|
24
|
+
|
|
25
|
+
ab = ab.slice(0, 16)
|
|
26
|
+
ab[ab.length - 1] += 0x01
|
|
27
|
+
|
|
28
|
+
return buf2hex(ab)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function pair_verify_aes_key(shared) {
|
|
32
|
+
return buf2hex(
|
|
33
|
+
crypto.createHash('sha512')
|
|
34
|
+
.update('Pair-Verify-AES-Key')
|
|
35
|
+
.update(hexString2ArrayBuffer(shared))
|
|
36
|
+
.digest()
|
|
37
|
+
.slice(0, 16),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pair_verify_aes_iv(shared) {
|
|
42
|
+
return buf2hex(
|
|
43
|
+
crypto.createHash('sha512')
|
|
44
|
+
.update('Pair-Verify-AES-IV')
|
|
45
|
+
.update(hexString2ArrayBuffer(shared))
|
|
46
|
+
.digest()
|
|
47
|
+
.slice(0, 16),
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ...
|
|
52
|
+
// Public.
|
|
53
|
+
|
|
54
|
+
function a_pub(a) {
|
|
55
|
+
return ed25519.publicKey(a)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function confirm(a, K) {
|
|
59
|
+
const key = pair_setup_aes_key(K)
|
|
60
|
+
const iv = pair_setup_aes_iv(K)
|
|
61
|
+
|
|
62
|
+
const cipher = crypto.createCipheriv(
|
|
63
|
+
'aes-128-gcm',
|
|
64
|
+
hexString2ArrayBuffer(key),
|
|
65
|
+
hexString2ArrayBuffer(iv),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
let encrypted = cipher.update(hexString2ArrayBuffer(a_pub(a)), null, 'hex')
|
|
69
|
+
encrypted += cipher.final('hex')
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
epk: encrypted,
|
|
73
|
+
authTag: buf2hex(cipher.getAuthTag()),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function verifier(a) {
|
|
78
|
+
const keyPair = axlsign.generateKeyPair(crypto.randomBytes(32))
|
|
79
|
+
const v_pri = buf2hex(keyPair.private)
|
|
80
|
+
const v_pub = buf2hex(keyPair.public)
|
|
81
|
+
|
|
82
|
+
const header = Buffer.from([0x01, 0x00, 0x00, 0x00])
|
|
83
|
+
const a_pub_buf = Buffer.from(a_pub(a), 'hex')
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
verifierBody: Buffer.concat(
|
|
87
|
+
[header, keyPair.public, a_pub_buf],
|
|
88
|
+
header.byteLength + keyPair.public.byteLength + a_pub_buf.byteLength,
|
|
89
|
+
),
|
|
90
|
+
v_pri,
|
|
91
|
+
v_pub,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function shared(v_pri, atv_pub) {
|
|
96
|
+
return buf2hex(
|
|
97
|
+
axlsign.sharedKey(
|
|
98
|
+
hexString2ArrayBuffer(v_pri),
|
|
99
|
+
hexString2ArrayBuffer(atv_pub),
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function signed(a, v_pub, atv_pub) {
|
|
105
|
+
return ed25519.sign(a, v_pub + atv_pub)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function signature(shared, atv_data, signed) {
|
|
109
|
+
const cipher = crypto.createCipheriv(
|
|
110
|
+
'aes-128-ctr',
|
|
111
|
+
hexString2ArrayBuffer(pair_verify_aes_key(shared)),
|
|
112
|
+
hexString2ArrayBuffer(pair_verify_aes_iv(shared)),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// discard the result of encrypting atv_data.
|
|
116
|
+
cipher.update(hexString2ArrayBuffer(atv_data))
|
|
117
|
+
|
|
118
|
+
let encrypted = cipher.update(Buffer.from(signed, 'hex'), null, 'hex')
|
|
119
|
+
encrypted += cipher.final('hex')
|
|
120
|
+
|
|
121
|
+
return encrypted
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
pair_setup_aes_key,
|
|
126
|
+
pair_setup_aes_iv,
|
|
127
|
+
pair_verify_aes_key,
|
|
128
|
+
pair_verify_aes_iv,
|
|
129
|
+
|
|
130
|
+
a_pub,
|
|
131
|
+
confirm,
|
|
132
|
+
verifier,
|
|
133
|
+
shared,
|
|
134
|
+
signed,
|
|
135
|
+
signature,
|
|
136
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const events = require('events')
|
|
2
|
+
const util = require('util')
|
|
3
|
+
const config = require('./config')
|
|
4
|
+
const nu = require('./num_util')
|
|
5
|
+
|
|
6
|
+
const SEQ_NUM_WRAP = Math.pow(2, 16)
|
|
7
|
+
|
|
8
|
+
function AudioOut() {
|
|
9
|
+
events.EventEmitter.call(this)
|
|
10
|
+
|
|
11
|
+
this.lastSeq = -1
|
|
12
|
+
this.hasAirTunes = false
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
util.inherits(AudioOut, events.EventEmitter)
|
|
16
|
+
|
|
17
|
+
AudioOut.prototype.init = function (devices, circularBuffer) {
|
|
18
|
+
var self = this
|
|
19
|
+
config.rtp_time_ref = new Date().getTime()
|
|
20
|
+
|
|
21
|
+
devices.on('airtunes_devices', function (hasAirTunes) {
|
|
22
|
+
self.hasAirTunes = hasAirTunes
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
devices.on('need_sync', function () {
|
|
26
|
+
// a sync is forced when a new remote device is added
|
|
27
|
+
self.emit('need_sync', self.lastSeq)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
function sendPacket(seq) {
|
|
31
|
+
var packet = circularBuffer.readPacket()
|
|
32
|
+
|
|
33
|
+
packet.seq = seq % SEQ_NUM_WRAP
|
|
34
|
+
packet.timestamp = nu.low32(seq * config.frames_per_packet + 2 * config.sampling_rate)
|
|
35
|
+
|
|
36
|
+
if (self.hasAirTunes && seq % config.sync_period == 0)
|
|
37
|
+
self.emit('need_sync', seq)
|
|
38
|
+
|
|
39
|
+
self.emit('packet', packet)
|
|
40
|
+
packet.release()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function syncAudio() {
|
|
44
|
+
/*
|
|
45
|
+
* Each time syncAudio() runs, a burst of packet is sent.
|
|
46
|
+
* Increasing config.stream_latency lowers CPU usage but increases the size of the burst.
|
|
47
|
+
* If the burst size exceeds the UDP windows size (which we do not know), packets are lost.
|
|
48
|
+
*/
|
|
49
|
+
var elapsed = new Date().getTime() - config.rtp_time_ref
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* currentSeq is the # of the packet we should be sending now. We have some packets to catch-up
|
|
53
|
+
* since syncAudio is not always running.
|
|
54
|
+
*/
|
|
55
|
+
var currentSeq = Math.floor(elapsed * config.sampling_rate / (config.frames_per_packet * 1000))
|
|
56
|
+
|
|
57
|
+
for (let i = self.lastSeq + 1; i <= currentSeq; i++)
|
|
58
|
+
sendPacket(i)
|
|
59
|
+
|
|
60
|
+
self.lastSeq = currentSeq
|
|
61
|
+
|
|
62
|
+
// reschedule ourselves later
|
|
63
|
+
setTimeout(syncAudio, config.stream_latency)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
syncAudio()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = AudioOut
|