@ciphore/radiocli 0.2.4 → 0.2.5
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 +70 -2
- package/CONTRIBUTING.md +10 -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 +178 -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 +113 -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 +249 -76
- package/dist/ui/AdaptiveContent.js +18 -10
- 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/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/SettingsScreen.js +16 -16
- package/dist/ui/screens/StatsScreen.js +1 -1
- package/dist/ui/text-direction.js +9 -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/gallop.js +1 -1
- package/dist/ui/visualizers/receiver-style-registry.js +2 -1
- package/dist/update-check.js +2 -1
- package/docs/THIRD_PARTY_NOTICES.md +6 -4
- package/package.json +34 -11
- 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,266 @@
|
|
|
1
|
+
const events = require('events')
|
|
2
|
+
const util = require('util')
|
|
3
|
+
const async = require('async')
|
|
4
|
+
const AirTunesDevice = require('./device_airtunes')
|
|
5
|
+
|
|
6
|
+
function Devices(audioOut) {
|
|
7
|
+
events.EventEmitter.call(this)
|
|
8
|
+
|
|
9
|
+
this.source = null
|
|
10
|
+
this.devices = {}
|
|
11
|
+
this.hasAirTunes = false
|
|
12
|
+
this.audioOut = audioOut
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
util.inherits(Devices, events.EventEmitter)
|
|
16
|
+
|
|
17
|
+
Devices.prototype.init = function () {
|
|
18
|
+
var self = this
|
|
19
|
+
self.audioOut.on('need_sync', function (seq) {
|
|
20
|
+
// relay to all devices
|
|
21
|
+
|
|
22
|
+
self.forEach(function (dev) {
|
|
23
|
+
try {
|
|
24
|
+
if (dev.onSyncNeeded && dev.controlPort)
|
|
25
|
+
dev.onSyncNeeded(seq)
|
|
26
|
+
}
|
|
27
|
+
catch (e) {}
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Devices.prototype.forEach = function (it) {
|
|
33
|
+
for (const i in this.devices) {
|
|
34
|
+
if (!this.devices.hasOwnProperty(i))
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
it(this.devices[i], i)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Devices.prototype.add = function (type, host, options) {
|
|
42
|
+
var self = this
|
|
43
|
+
|
|
44
|
+
this.status = 'connecting'
|
|
45
|
+
var dev
|
|
46
|
+
// type === 'coreaudio' ?
|
|
47
|
+
// new CoreAudioDevice(this.hasAirTunes, this.audioOut, options) :
|
|
48
|
+
= new AirTunesDevice(host, this.audioOut, options, options?.mode ?? 0, options?.txt ?? [])
|
|
49
|
+
|
|
50
|
+
var previousDev = this.devices[dev.key]
|
|
51
|
+
|
|
52
|
+
if (previousDev) {
|
|
53
|
+
// if device is already in the pool, just report its existing status.
|
|
54
|
+
previousDev.reportStatus()
|
|
55
|
+
|
|
56
|
+
return previousDev
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.devices[dev.key] = dev
|
|
60
|
+
|
|
61
|
+
dev.on('status', function (status, arg) {
|
|
62
|
+
if (status === 'error' || status === 'stopped') {
|
|
63
|
+
delete self.devices[dev.key]
|
|
64
|
+
self.checkAirTunesDevices()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (this.hasAirTunes && status === 'playing') {
|
|
68
|
+
self.emit('need_sync')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
self.emit('status', dev.key, status, '')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
dev.start()
|
|
75
|
+
self.checkAirTunesDevices()
|
|
76
|
+
|
|
77
|
+
return dev
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
Devices.prototype.setVolume = function (key, volume, callback) {
|
|
81
|
+
var dev = this.devices[key]
|
|
82
|
+
|
|
83
|
+
if (!dev) {
|
|
84
|
+
this.emit('status', key, 'error', 'not_found')
|
|
85
|
+
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
dev.setVolume(volume, callback)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Devices.prototype.setProgress = function (key, progress, duration, callback) {
|
|
93
|
+
try {
|
|
94
|
+
if (key === 'all') {
|
|
95
|
+
for (const device of Object.keys(this.devices)) {
|
|
96
|
+
try {
|
|
97
|
+
this.devices[device].setProgress(progress, duration, callback)
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
try {
|
|
101
|
+
if (e.name === 'TypeError')
|
|
102
|
+
delete this.devices[device]
|
|
103
|
+
}
|
|
104
|
+
catch (e) {}
|
|
105
|
+
|
|
106
|
+
console.log(e)
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
var dev = this.devices[key]
|
|
112
|
+
|
|
113
|
+
if (!dev) {
|
|
114
|
+
this.emit('status', key, 'error', 'not_found')
|
|
115
|
+
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
dev.setProgress(progress, duration, callback)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (e) { console.log(e) }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
Devices.prototype.setTrackInfo = function (key, name, artist, album, callback) {
|
|
126
|
+
try {
|
|
127
|
+
if (key === 'all') {
|
|
128
|
+
for (const device of Object.keys(this.devices)) {
|
|
129
|
+
try {
|
|
130
|
+
this.devices[device].setTrackInfo(name, artist, album, callback)
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
try {
|
|
134
|
+
if (e.name === 'TypeError')
|
|
135
|
+
delete this.devices[device]
|
|
136
|
+
}
|
|
137
|
+
catch (e) {}
|
|
138
|
+
console.log(e)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
var dev = this.devices[key]
|
|
144
|
+
|
|
145
|
+
if (!dev) {
|
|
146
|
+
this.emit('status', key, 'error', 'not_found')
|
|
147
|
+
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
dev.setTrackInfo(name, artist, album, callback)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (e) { console.log(e) }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
Devices.prototype.setArtwork = function (key, art, contentType, callback) {
|
|
159
|
+
try {
|
|
160
|
+
if (key === 'all') {
|
|
161
|
+
for (const device of Object.keys(this.devices)) {
|
|
162
|
+
try {
|
|
163
|
+
this.devices[device].setArtwork(art, contentType, callback)
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
try {
|
|
167
|
+
if (e.name === 'TypeError')
|
|
168
|
+
delete this.devices[device]
|
|
169
|
+
}
|
|
170
|
+
catch (e) {}
|
|
171
|
+
console.log(e)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
var dev = this.devices[key]
|
|
177
|
+
|
|
178
|
+
if (!dev) {
|
|
179
|
+
this.emit('status', key, 'error', 'not_found')
|
|
180
|
+
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
dev.setArtwork(art, contentType, callback)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (e) { console.log(e) }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
Devices.prototype.setPasscode = function (key, passcode) {
|
|
191
|
+
var dev = this.devices[key]
|
|
192
|
+
|
|
193
|
+
if (!dev) {
|
|
194
|
+
this.emit('status', key, 'error', 'not_found')
|
|
195
|
+
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
dev.setPasscode(passcode)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
Devices.prototype.stop = function (key) {
|
|
203
|
+
var dev = this.devices[key]
|
|
204
|
+
if (!dev) {
|
|
205
|
+
this.emit('status', key, 'error', 'not_found')
|
|
206
|
+
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
dev.stop()
|
|
210
|
+
this.devices[key] = undefined
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
Devices.prototype.stopAll = function (allCb) {
|
|
214
|
+
// conver to array to make async happy
|
|
215
|
+
var devices = []
|
|
216
|
+
for (const i in this.devices) {
|
|
217
|
+
if (!this.devices.hasOwnProperty(i))
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
devices.push(this.devices[i])
|
|
221
|
+
}
|
|
222
|
+
if (allCb != null) {
|
|
223
|
+
async.forEach(
|
|
224
|
+
devices,
|
|
225
|
+
function (dev, cb) {
|
|
226
|
+
dev.stop(cb)
|
|
227
|
+
},
|
|
228
|
+
function () {
|
|
229
|
+
this.devices = {}
|
|
230
|
+
allCb()
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
Devices.prototype.checkAirTunesDevices = function () {
|
|
237
|
+
try {
|
|
238
|
+
var newHasAirTunes = false
|
|
239
|
+
|
|
240
|
+
for (const key in this.devices) {
|
|
241
|
+
if (!this.devices.hasOwnProperty(key))
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
var device = this.devices[key]
|
|
245
|
+
|
|
246
|
+
// if(device.type === 'airtunes') {
|
|
247
|
+
newHasAirTunes = true
|
|
248
|
+
break
|
|
249
|
+
// }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (newHasAirTunes !== this.hasAirTunes) {
|
|
253
|
+
this.emit('airtunes_devices', newHasAirTunes)
|
|
254
|
+
|
|
255
|
+
this.forEach(function (dev) {
|
|
256
|
+
if (dev.setHasAirTunes)
|
|
257
|
+
dev.setHasAirTunes(newHasAirTunes)
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.hasAirTunes = newHasAirTunes
|
|
262
|
+
}
|
|
263
|
+
catch (e) { console.log(e) }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = Devices
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// RadioCLI modification, 2026-10-16. Distributed under the accompanying AGPLv3.
|
|
2
|
+
// Preserve the sender's synchronous hex API using Node's Ed25519 implementation.
|
|
3
|
+
const crypto = require('node:crypto')
|
|
4
|
+
const pkcs8Prefix = Buffer.from('302e020100300506032b657004220420', 'hex')
|
|
5
|
+
|
|
6
|
+
function bytes(hex, length) {
|
|
7
|
+
if (typeof hex !== 'string' || !/^(?:[a-f\d]{2})*$/i.test(hex) || (length !== undefined && hex.length !== length * 2)) {
|
|
8
|
+
throw new TypeError('Invalid Ed25519 hexadecimal input')
|
|
9
|
+
}
|
|
10
|
+
return Buffer.from(hex, 'hex')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function privateKey(seed) {
|
|
14
|
+
return crypto.createPrivateKey({key: Buffer.concat([pkcs8Prefix, bytes(seed, 32)]), format: 'der', type: 'pkcs8'})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function publicKey(seed) {
|
|
18
|
+
const key = crypto.createPublicKey(privateKey(seed)).export({format: 'jwk'})
|
|
19
|
+
return Buffer.from(key.x, 'base64url').toString('hex')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sign(seed, message) {
|
|
23
|
+
return crypto.sign(null, bytes(message), privateKey(seed)).toString('hex').toUpperCase()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {publicKey, sign}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
Object.defineProperty(exports, '__esModule', { value: true })
|
|
3
|
+
const struct = require('python-struct')
|
|
4
|
+
const encryption_1 = require('./encryption')
|
|
5
|
+
|
|
6
|
+
class Credentials {
|
|
7
|
+
constructor(uniqueIdentifier, identifier, pairingId, publicKey, encryptionKey) {
|
|
8
|
+
this.uniqueIdentifier = uniqueIdentifier
|
|
9
|
+
this.identifier = identifier
|
|
10
|
+
this.pairingId = pairingId
|
|
11
|
+
this.publicKey = publicKey
|
|
12
|
+
this.encryptionKey = encryptionKey
|
|
13
|
+
this.encryptCount = 0
|
|
14
|
+
this.decryptCount = 0
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a credentials string into a Credentials object.
|
|
19
|
+
* @param text The credentials string.
|
|
20
|
+
* @returns A credentials object.
|
|
21
|
+
*/
|
|
22
|
+
static parse(text) {
|
|
23
|
+
let parts = text.split(':')
|
|
24
|
+
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'))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns a string representation of a Credentials object.
|
|
29
|
+
* @returns A string representation of a Credentials object.
|
|
30
|
+
*/
|
|
31
|
+
toString() {
|
|
32
|
+
return this.uniqueIdentifier
|
|
33
|
+
+ ':'
|
|
34
|
+
+ this.identifier.toString('hex')
|
|
35
|
+
+ ':'
|
|
36
|
+
+ Buffer.from(this.pairingId).toString('hex')
|
|
37
|
+
+ ':'
|
|
38
|
+
+ this.publicKey.toString('hex')
|
|
39
|
+
+ ':'
|
|
40
|
+
+ this.encryptionKey.toString('hex')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
encrypt(message) {
|
|
44
|
+
let offset = 0
|
|
45
|
+
let total = message.byteLength
|
|
46
|
+
let result = Buffer.concat([])
|
|
47
|
+
while (offset < total) {
|
|
48
|
+
let length = Math.min(total - offset, 1024)
|
|
49
|
+
let s1length_bytes = struct.pack('H', length)
|
|
50
|
+
// let cipher = crypto.createCipheriv('chacha20-poly1305', this.writeKey, Buffer.concat([Buffer.from([0x00,0x00,0x00,0x00]),struct.pack("Q", this.decryptCount)]), { authTagLength: 16 });
|
|
51
|
+
// cipher.setAAD(s1length_bytes);
|
|
52
|
+
// let s1ct = cipher.update(message);
|
|
53
|
+
// cipher.final();
|
|
54
|
+
// let s1tag = encryption_1.default.computePoly1305(s1ct,s1length_bytes,Buffer.concat([Buffer.from([0x00,0x00,0x00,0x00]),struct.pack("Q", this.decryptCount)]),this.writeKey)
|
|
55
|
+
let [s1ct, s1tag] = encryption_1.default.encryptAndSeal(message.slice(offset, offset + length), s1length_bytes, Buffer.concat([Buffer.from([0x00, 0x00, 0x00, 0x00]), struct.pack('Q', this.encryptCount)]), this.writeKey)
|
|
56
|
+
|
|
57
|
+
let ciphertext = Buffer.concat([s1length_bytes, s1ct, s1tag])
|
|
58
|
+
offset += length
|
|
59
|
+
this.encryptCount += 1
|
|
60
|
+
result = Buffer.concat([result, ciphertext])
|
|
61
|
+
}
|
|
62
|
+
return result
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
decrypt(message) {
|
|
66
|
+
let offset = 0
|
|
67
|
+
let result = Buffer.concat([])
|
|
68
|
+
while (offset < message.byteLength) {
|
|
69
|
+
let lengthbytes = message.slice(offset, offset + 2)
|
|
70
|
+
let length = struct.unpack('H', lengthbytes)
|
|
71
|
+
let messagea = message.slice(offset + 2, offset + 2 + length[0] + 16)
|
|
72
|
+
let cipherText = messagea.slice(0, -16)
|
|
73
|
+
let hmac = messagea.slice(-16)
|
|
74
|
+
let decrypted = encryption_1.default.verifyAndDecrypt(cipherText, hmac, lengthbytes, Buffer.concat([Buffer.from([0x00, 0x00, 0x00, 0x00]), struct.pack('Q', this.decryptCount)]), this.readKey)
|
|
75
|
+
this.decryptCount += 1
|
|
76
|
+
offset = offset + length[0] + 16 + 2
|
|
77
|
+
result = Buffer.concat([result, decrypted])
|
|
78
|
+
}
|
|
79
|
+
return result
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
encryptAudio(message, aad, nonce) {
|
|
83
|
+
return Buffer.concat([Buffer.concat(encryption_1.default.encryptAndSeal(message, aad, struct.pack('Q', nonce), this.writeKey)), Buffer.from(struct.pack('Q', nonce))])
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.Credentials = Credentials
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
Object.defineProperty(exports, '__esModule', { value: true })
|
|
3
|
+
const chacha = require('chacha-js')
|
|
4
|
+
const crypto = require('crypto')
|
|
5
|
+
// i'd really prefer for this to be a direct call to
|
|
6
|
+
// Sodium.crypto_aead_chacha20poly1305_decrypt()
|
|
7
|
+
// but unfortunately the way it constructs the message to
|
|
8
|
+
// calculate the HMAC is not compatible with homekit
|
|
9
|
+
// (long story short, it uses [ AAD, AAD.length, CipherText, CipherText.length ]
|
|
10
|
+
// whereas homekit expects [ AAD, CipherText, AAD.length, CipherText.length ]
|
|
11
|
+
function verifyAndDecrypt(cipherText, mac, AAD, nonce, key) {
|
|
12
|
+
try {
|
|
13
|
+
if (nonce.byteLength == 8) nonce = Buffer.concat([Buffer.from([0x00, 0x00, 0x00, 0x00]), nonce])
|
|
14
|
+
const decipher = chacha.createDecipher(key, nonce)
|
|
15
|
+
if (AAD != null) decipher.setAAD(AAD)// must be called before data
|
|
16
|
+
decipher.setAuthTag(mac)
|
|
17
|
+
return decipher.update(cipherText)
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function encryptAndSeal(plainText, AAD, nonce, key) {
|
|
25
|
+
if (nonce.byteLength == 8) nonce = Buffer.concat([Buffer.from([0x00, 0x00, 0x00, 0x00]), nonce])
|
|
26
|
+
var cipher = chacha.createCipher(key, nonce)
|
|
27
|
+
if (AAD != null) cipher.setAAD(AAD)// must be called before data
|
|
28
|
+
let cipherText = cipher.update(plainText)
|
|
29
|
+
cipher.final()
|
|
30
|
+
let hmac = cipher.getAuthTag()
|
|
31
|
+
return [cipherText, hmac]
|
|
32
|
+
}
|
|
33
|
+
// function getPadding(buffer, blockSize) {
|
|
34
|
+
// return buffer.length % blockSize === 0
|
|
35
|
+
// ? Buffer.alloc(0)
|
|
36
|
+
// : Buffer.alloc(blockSize - (buffer.length % blockSize));
|
|
37
|
+
// }
|
|
38
|
+
function HKDF(hashAlg, salt, ikm, info, size) {
|
|
39
|
+
// create the hash alg to see if it exists and get its length
|
|
40
|
+
var hash = crypto.createHash(hashAlg)
|
|
41
|
+
var hashLength = hash.digest().length
|
|
42
|
+
// now we compute the PRK
|
|
43
|
+
var hmac = crypto.createHmac(hashAlg, salt)
|
|
44
|
+
hmac.update(ikm)
|
|
45
|
+
var prk = hmac.digest()
|
|
46
|
+
var prev = Buffer.alloc(0)
|
|
47
|
+
var output
|
|
48
|
+
var buffers = []
|
|
49
|
+
var num_blocks = Math.ceil(size / hashLength)
|
|
50
|
+
info = Buffer.from(info)
|
|
51
|
+
for (var i = 0; i < num_blocks; i++) {
|
|
52
|
+
var hmac = crypto.createHmac(hashAlg, prk)
|
|
53
|
+
var input = Buffer.concat([
|
|
54
|
+
prev,
|
|
55
|
+
info,
|
|
56
|
+
Buffer.from(String.fromCharCode(i + 1)),
|
|
57
|
+
])
|
|
58
|
+
hmac.update(input)
|
|
59
|
+
prev = hmac.digest()
|
|
60
|
+
buffers.push(prev)
|
|
61
|
+
}
|
|
62
|
+
output = Buffer.concat(buffers, size)
|
|
63
|
+
return output.slice(0, size)
|
|
64
|
+
}
|
|
65
|
+
exports.default = {
|
|
66
|
+
encryptAndSeal,
|
|
67
|
+
verifyAndDecrypt,
|
|
68
|
+
HKDF,
|
|
69
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
Object.defineProperty(exports, '__esModule', { value: true })
|
|
3
|
+
const assert = require('assert')
|
|
4
|
+
/*
|
|
5
|
+
* Originally based on code from github:KhaosT/HAP-NodeJS@0c8fd88 used
|
|
6
|
+
* used per the terms of the Apache Software License v2.
|
|
7
|
+
*
|
|
8
|
+
* Original code copyright Khaos Tian <khaos.tian@gmail.com>
|
|
9
|
+
*
|
|
10
|
+
* Modifications copyright Zach Bean <zb@forty2.com>
|
|
11
|
+
* * Reformatted for ES6-style module
|
|
12
|
+
* * renamed *UInt64* to *UInt53* to be more clear about range
|
|
13
|
+
* * renamed uintHighLow to be more clear about what it does
|
|
14
|
+
* * Refactored to return a buffer rather write into a passed-in buffer
|
|
15
|
+
*/
|
|
16
|
+
function splitUInt53(number) {
|
|
17
|
+
const MAX_UINT32 = 0x00000000FFFFFFFF
|
|
18
|
+
const MAX_INT53 = 0x001FFFFFFFFFFFFF
|
|
19
|
+
assert(number > -1 && number <= MAX_INT53, 'number out of range')
|
|
20
|
+
assert(Math.floor(number) === number, 'number must be an integer')
|
|
21
|
+
let high = 0
|
|
22
|
+
const signbit = number & 0xFFFFFFFF
|
|
23
|
+
const low = signbit < 0 ? (number & 0x7FFFFFFF) + 0x80000000 : signbit
|
|
24
|
+
if (number > MAX_UINT32) {
|
|
25
|
+
high = (number - low) / (MAX_UINT32 + 1)
|
|
26
|
+
}
|
|
27
|
+
return [high, low]
|
|
28
|
+
}
|
|
29
|
+
function UInt53toBufferLE(number) {
|
|
30
|
+
const [high, low] = splitUInt53(number)
|
|
31
|
+
const buf = Buffer.alloc(8)
|
|
32
|
+
buf.writeUInt32LE(low, 0)
|
|
33
|
+
buf.writeUInt32LE(high, 4)
|
|
34
|
+
return buf
|
|
35
|
+
}
|
|
36
|
+
function UInt16toBufferBE(number) {
|
|
37
|
+
const buf = Buffer.alloc(2)
|
|
38
|
+
buf.writeUInt16BE(number, 0)
|
|
39
|
+
return buf
|
|
40
|
+
}
|
|
41
|
+
exports.default = {
|
|
42
|
+
UInt53toBufferLE,
|
|
43
|
+
UInt16toBufferBE,
|
|
44
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/**
|
|
3
|
+
* Type Length Value encoding/decoding, used by HAP as a wire format.
|
|
4
|
+
* https://en.wikipedia.org/wiki/Type-length-value
|
|
5
|
+
*
|
|
6
|
+
* Originally based on code from github:KhaosT/HAP-NodeJS@0c8fd88 used
|
|
7
|
+
* used per the terms of the Apache Software License v2.
|
|
8
|
+
*
|
|
9
|
+
* Original code copyright Khaos Tian <khaos.tian@gmail.com>
|
|
10
|
+
*
|
|
11
|
+
* Modifications copyright Zach Bean <zb@forty2.com>
|
|
12
|
+
* * Reformatted for ES6-style module
|
|
13
|
+
* * Rewrote encode() to be non-recursive; also simplified the logic
|
|
14
|
+
* * Rewrote decode()
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, '__esModule', { value: true })
|
|
17
|
+
const Tag = {
|
|
18
|
+
PairingMethod: 0x00,
|
|
19
|
+
Username: 0x01,
|
|
20
|
+
Salt: 0x02,
|
|
21
|
+
// could be either the SRP client public key (384 bytes) or the ED25519 public key (32 bytes), depending on context
|
|
22
|
+
PublicKey: 0x03,
|
|
23
|
+
Proof: 0x04,
|
|
24
|
+
EncryptedData: 0x05,
|
|
25
|
+
Sequence: 0x06,
|
|
26
|
+
ErrorCode: 0x07,
|
|
27
|
+
BackOff: 0x08,
|
|
28
|
+
Signature: 0x0A,
|
|
29
|
+
MFiCertificate: 0x09,
|
|
30
|
+
MFiSignature: 0x0A,
|
|
31
|
+
Flags: 0x13,
|
|
32
|
+
}
|
|
33
|
+
function encode(type, data) {
|
|
34
|
+
var encodedTLVBuffer = Buffer.alloc(0)
|
|
35
|
+
// coerce data to Buffer if needed
|
|
36
|
+
if (typeof data === 'number')
|
|
37
|
+
data = Buffer.from([data])
|
|
38
|
+
else if (typeof data === 'string')
|
|
39
|
+
data = Buffer.from(data)
|
|
40
|
+
if (data.length <= 255) {
|
|
41
|
+
encodedTLVBuffer = Buffer.concat([Buffer.from([type, data.length]), data])
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
var leftLength = data.length
|
|
45
|
+
var tempBuffer = Buffer.alloc(0)
|
|
46
|
+
var currentStart = 0
|
|
47
|
+
for (; leftLength > 0;) {
|
|
48
|
+
if (leftLength >= 255) {
|
|
49
|
+
tempBuffer = Buffer.concat([tempBuffer, Buffer.from([type, 0xFF]), data.slice(currentStart, currentStart + 255)])
|
|
50
|
+
leftLength -= 255
|
|
51
|
+
currentStart = currentStart + 255
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
tempBuffer = Buffer.concat([tempBuffer, Buffer.from([type, leftLength]), data.slice(currentStart, currentStart + leftLength)])
|
|
55
|
+
leftLength -= leftLength
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
encodedTLVBuffer = tempBuffer
|
|
59
|
+
}
|
|
60
|
+
// do we have more to encode?
|
|
61
|
+
if (arguments.length > 2) {
|
|
62
|
+
// chop off the first two arguments which we already processed, and process the rest recursively
|
|
63
|
+
var remainingArguments = Array.prototype.slice.call(arguments, 2)
|
|
64
|
+
var remainingTLVBuffer = encode.apply(this, remainingArguments)
|
|
65
|
+
// append the remaining encoded arguments directly to the buffer
|
|
66
|
+
encodedTLVBuffer = Buffer.concat([encodedTLVBuffer, remainingTLVBuffer])
|
|
67
|
+
}
|
|
68
|
+
return encodedTLVBuffer
|
|
69
|
+
}
|
|
70
|
+
function decode(data) {
|
|
71
|
+
const objects = {}
|
|
72
|
+
let leftLength = data.length
|
|
73
|
+
let currentIndex = 0
|
|
74
|
+
for (; leftLength > 0;) {
|
|
75
|
+
const type = data[currentIndex]
|
|
76
|
+
const length = data[currentIndex + 1]
|
|
77
|
+
currentIndex += 2
|
|
78
|
+
leftLength -= 2
|
|
79
|
+
const newData = data.slice(currentIndex, currentIndex + length)
|
|
80
|
+
if (objects[type]) {
|
|
81
|
+
objects[type] = Buffer.concat([objects[type], newData])
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
objects[type] = newData
|
|
85
|
+
}
|
|
86
|
+
currentIndex += length
|
|
87
|
+
leftLength -= length
|
|
88
|
+
}
|
|
89
|
+
return objects
|
|
90
|
+
}
|
|
91
|
+
exports.default = {
|
|
92
|
+
Tag,
|
|
93
|
+
encode,
|
|
94
|
+
decode,
|
|
95
|
+
}
|