pal-explorer-cli 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +18 -0
- package/README.md +314 -0
- package/bin/pal.js +230 -0
- package/extensions/@palexplorer/analytics/README.md +45 -0
- package/extensions/@palexplorer/analytics/docs/MONETIZATION.md +14 -0
- package/extensions/@palexplorer/analytics/docs/PLAN.md +23 -0
- package/extensions/@palexplorer/analytics/docs/PRIVACY.md +38 -0
- package/extensions/@palexplorer/analytics/extension.json +27 -0
- package/extensions/@palexplorer/analytics/index.js +186 -0
- package/extensions/@palexplorer/analytics/test/analytics.test.js +82 -0
- package/extensions/@palexplorer/audit/extension.json +17 -0
- package/extensions/@palexplorer/audit/index.js +2 -0
- package/extensions/@palexplorer/auth-email/extension.json +17 -0
- package/extensions/@palexplorer/auth-email/index.js +102 -0
- package/extensions/@palexplorer/auth-oauth/extension.json +16 -0
- package/extensions/@palexplorer/auth-oauth/index.js +199 -0
- package/extensions/@palexplorer/chat/extension.json +17 -0
- package/extensions/@palexplorer/chat/index.js +2 -0
- package/extensions/@palexplorer/discovery/extension.json +16 -0
- package/extensions/@palexplorer/discovery/index.js +111 -0
- package/extensions/@palexplorer/email-notifications/extension.json +23 -0
- package/extensions/@palexplorer/email-notifications/index.js +242 -0
- package/extensions/@palexplorer/explorer-integration/extension.json +13 -0
- package/extensions/@palexplorer/explorer-integration/index.js +122 -0
- package/extensions/@palexplorer/groups/extension.json +17 -0
- package/extensions/@palexplorer/groups/index.js +2 -0
- package/extensions/@palexplorer/networks/extension.json +17 -0
- package/extensions/@palexplorer/networks/index.js +2 -0
- package/extensions/@palexplorer/share-links/extension.json +17 -0
- package/extensions/@palexplorer/share-links/index.js +2 -0
- package/extensions/@palexplorer/sync/extension.json +17 -0
- package/extensions/@palexplorer/sync/index.js +2 -0
- package/extensions/@palexplorer/user-mgmt/extension.json +17 -0
- package/extensions/@palexplorer/user-mgmt/index.js +2 -0
- package/extensions/@palexplorer/vfs/extension.json +17 -0
- package/extensions/@palexplorer/vfs/index.js +167 -0
- package/lib/capabilities.js +263 -0
- package/lib/commands/analytics.js +175 -0
- package/lib/commands/api-keys.js +131 -0
- package/lib/commands/audit.js +235 -0
- package/lib/commands/auth.js +137 -0
- package/lib/commands/backup.js +76 -0
- package/lib/commands/billing.js +148 -0
- package/lib/commands/chat.js +217 -0
- package/lib/commands/cloud-backup.js +231 -0
- package/lib/commands/comment.js +99 -0
- package/lib/commands/completion.js +203 -0
- package/lib/commands/compliance.js +218 -0
- package/lib/commands/config.js +136 -0
- package/lib/commands/connect.js +44 -0
- package/lib/commands/dept.js +294 -0
- package/lib/commands/device.js +146 -0
- package/lib/commands/download.js +226 -0
- package/lib/commands/explorer.js +178 -0
- package/lib/commands/extension.js +970 -0
- package/lib/commands/favorite.js +90 -0
- package/lib/commands/federation.js +270 -0
- package/lib/commands/file.js +533 -0
- package/lib/commands/group.js +271 -0
- package/lib/commands/gui-share.js +29 -0
- package/lib/commands/init.js +61 -0
- package/lib/commands/invite.js +59 -0
- package/lib/commands/list.js +59 -0
- package/lib/commands/log.js +116 -0
- package/lib/commands/nearby.js +108 -0
- package/lib/commands/network.js +251 -0
- package/lib/commands/notify.js +198 -0
- package/lib/commands/org.js +273 -0
- package/lib/commands/pal.js +180 -0
- package/lib/commands/permissions.js +216 -0
- package/lib/commands/pin.js +97 -0
- package/lib/commands/protocol.js +357 -0
- package/lib/commands/rbac.js +147 -0
- package/lib/commands/recover.js +36 -0
- package/lib/commands/register.js +171 -0
- package/lib/commands/relay.js +131 -0
- package/lib/commands/remote.js +368 -0
- package/lib/commands/revoke.js +50 -0
- package/lib/commands/scanner.js +280 -0
- package/lib/commands/schedule.js +344 -0
- package/lib/commands/scim.js +203 -0
- package/lib/commands/search.js +181 -0
- package/lib/commands/serve.js +438 -0
- package/lib/commands/server.js +350 -0
- package/lib/commands/share-link.js +199 -0
- package/lib/commands/share.js +323 -0
- package/lib/commands/sso.js +200 -0
- package/lib/commands/status.js +136 -0
- package/lib/commands/stream.js +562 -0
- package/lib/commands/su.js +187 -0
- package/lib/commands/sync.js +827 -0
- package/lib/commands/transfers.js +152 -0
- package/lib/commands/uninstall.js +188 -0
- package/lib/commands/update.js +204 -0
- package/lib/commands/user.js +276 -0
- package/lib/commands/vfs.js +84 -0
- package/lib/commands/web.js +52 -0
- package/lib/commands/webhook.js +180 -0
- package/lib/commands/whoami.js +59 -0
- package/lib/commands/workspace.js +121 -0
- package/lib/core/accessLog.js +54 -0
- package/lib/core/analytics.js +99 -0
- package/lib/core/backup.js +84 -0
- package/lib/core/billing.js +336 -0
- package/lib/core/bitfieldStore.js +53 -0
- package/lib/core/connectionManager.js +182 -0
- package/lib/core/dhtDiscovery.js +148 -0
- package/lib/core/discoveryClient.js +408 -0
- package/lib/core/extensionAnalyzer.js +357 -0
- package/lib/core/extensionSandbox.js +250 -0
- package/lib/core/extensionWorkerHost.js +166 -0
- package/lib/core/extensions.js +1082 -0
- package/lib/core/fileDiff.js +69 -0
- package/lib/core/groups.js +119 -0
- package/lib/core/identity.js +340 -0
- package/lib/core/mdnsService.js +126 -0
- package/lib/core/networks.js +81 -0
- package/lib/core/permissions.js +109 -0
- package/lib/core/pro.js +27 -0
- package/lib/core/resolver.js +74 -0
- package/lib/core/serverList.js +224 -0
- package/lib/core/sharePolicy.js +69 -0
- package/lib/core/shares.js +325 -0
- package/lib/core/signalingServer.js +441 -0
- package/lib/core/streamTransport.js +106 -0
- package/lib/core/su.js +55 -0
- package/lib/core/syncEngine.js +264 -0
- package/lib/core/syncState.js +159 -0
- package/lib/core/transfers.js +259 -0
- package/lib/core/users.js +225 -0
- package/lib/core/vfs.js +216 -0
- package/lib/core/webServer.js +702 -0
- package/lib/core/webrtcStream.js +396 -0
- package/lib/crypto/chatEncryption.js +57 -0
- package/lib/crypto/shareEncryption.js +195 -0
- package/lib/crypto/sharePassword.js +35 -0
- package/lib/crypto/streamEncryption.js +189 -0
- package/lib/package.json +1 -0
- package/lib/protocol/envelope.js +271 -0
- package/lib/protocol/handler.js +191 -0
- package/lib/protocol/index.js +27 -0
- package/lib/protocol/messages.js +247 -0
- package/lib/protocol/negotiation.js +127 -0
- package/lib/protocol/policy.js +142 -0
- package/lib/protocol/router.js +86 -0
- package/lib/protocol/sync.js +122 -0
- package/lib/utils/cli.js +15 -0
- package/lib/utils/config.js +123 -0
- package/lib/utils/configIntegrity.js +87 -0
- package/lib/utils/downloadDir.js +9 -0
- package/lib/utils/explorer.js +83 -0
- package/lib/utils/format.js +12 -0
- package/lib/utils/help.js +357 -0
- package/lib/utils/logger.js +103 -0
- package/lib/utils/torrent.js +203 -0
- package/package.json +71 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { getIceServersWithRelay, generateRoomId, getTransportConfig } from './streamTransport.js';
|
|
2
|
+
import { isPrivateUrl } from './discoveryClient.js';
|
|
3
|
+
import config from '../utils/config.js';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
|
|
6
|
+
// WebRTC streaming uses wrtc (node-webrtc) for Node.js / Electron main process
|
|
7
|
+
let wrtc;
|
|
8
|
+
try { wrtc = (await import('wrtc')).default; } catch {
|
|
9
|
+
// In Electron renderer or environments without wrtc, we'll use browser WebRTC APIs
|
|
10
|
+
wrtc = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RTCPeerConnection = wrtc?.RTCPeerConnection || globalThis.RTCPeerConnection;
|
|
14
|
+
const RTCSessionDescription = wrtc?.RTCSessionDescription || globalThis.RTCSessionDescription;
|
|
15
|
+
|
|
16
|
+
export class StreamPeer extends EventEmitter {
|
|
17
|
+
constructor({ signalingUrl, roomId, isInitiator = false, iceServers = [] }) {
|
|
18
|
+
super();
|
|
19
|
+
this.signalingUrl = signalingUrl;
|
|
20
|
+
this.roomId = roomId;
|
|
21
|
+
this.isInitiator = isInitiator;
|
|
22
|
+
this.iceServers = iceServers;
|
|
23
|
+
this.ws = null;
|
|
24
|
+
this.pc = null;
|
|
25
|
+
this.dataChannel = null;
|
|
26
|
+
this.peerId = null;
|
|
27
|
+
this.remotePeerId = null;
|
|
28
|
+
this._closed = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async connect() {
|
|
32
|
+
if (!RTCPeerConnection) throw new Error('WebRTC not available');
|
|
33
|
+
if (isPrivateUrl(this.signalingUrl)) throw new Error('SSRF blocked: private/internal signaling URL');
|
|
34
|
+
|
|
35
|
+
const wsUrl = this.signalingUrl.replace(/^http/, 'ws') + '/ws/signaling';
|
|
36
|
+
const WS = globalThis.WebSocket || (await import('ws')).default;
|
|
37
|
+
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
this.ws = new WS(wsUrl);
|
|
40
|
+
|
|
41
|
+
this.ws.onopen = () => {
|
|
42
|
+
this.ws.send(JSON.stringify({
|
|
43
|
+
type: 'join',
|
|
44
|
+
roomId: this.roomId,
|
|
45
|
+
publicKey: config.get('identity')?.publicKey || '',
|
|
46
|
+
}));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
this.ws.onmessage = async (event) => {
|
|
50
|
+
const msg = JSON.parse(typeof event.data === 'string' ? event.data : event.data.toString());
|
|
51
|
+
|
|
52
|
+
switch (msg.type) {
|
|
53
|
+
case 'connected':
|
|
54
|
+
this.peerId = msg.peerId;
|
|
55
|
+
break;
|
|
56
|
+
|
|
57
|
+
case 'joined':
|
|
58
|
+
if (this.isInitiator && msg.peers.length > 0) {
|
|
59
|
+
this.remotePeerId = msg.peers[0].peerId;
|
|
60
|
+
await this._createOffer();
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
|
|
64
|
+
case 'peer-joined':
|
|
65
|
+
this.remotePeerId = msg.peerId;
|
|
66
|
+
if (this.isInitiator) await this._createOffer();
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
case 'offer':
|
|
70
|
+
await this._handleOffer(msg);
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
case 'answer':
|
|
74
|
+
await this._handleAnswer(msg);
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case 'ice-candidate':
|
|
78
|
+
await this._handleIceCandidate(msg);
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case 'peer-left':
|
|
82
|
+
this.emit('peer-disconnected');
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case 'error':
|
|
86
|
+
reject(new Error(msg.error));
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.ws.onerror = (err) => reject(err);
|
|
92
|
+
this.ws.onclose = () => {
|
|
93
|
+
if (!this._closed) this.emit('signaling-disconnected');
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Resolve once the data channel is open
|
|
97
|
+
this.once('channel-open', () => resolve(this));
|
|
98
|
+
|
|
99
|
+
// Timeout after 30s
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
if (!this.dataChannel || this.dataChannel.readyState !== 'open') {
|
|
102
|
+
reject(new Error('WebRTC connection timeout'));
|
|
103
|
+
}
|
|
104
|
+
}, 30_000);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_createPeerConnection() {
|
|
109
|
+
this.pc = new RTCPeerConnection({ iceServers: this.iceServers });
|
|
110
|
+
|
|
111
|
+
this.pc.onicecandidate = (event) => {
|
|
112
|
+
if (event.candidate && this.ws?.readyState === 1) {
|
|
113
|
+
this.ws.send(JSON.stringify({
|
|
114
|
+
type: 'ice-candidate',
|
|
115
|
+
candidate: event.candidate,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
this.pc.oniceconnectionstatechange = () => {
|
|
121
|
+
const state = this.pc.iceConnectionState;
|
|
122
|
+
this.emit('ice-state', state);
|
|
123
|
+
if (state === 'connected' || state === 'completed') this.emit('connected');
|
|
124
|
+
if (state === 'failed' || state === 'disconnected') this.emit('disconnected');
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
this.pc.ondatachannel = (event) => {
|
|
128
|
+
this._setupDataChannel(event.channel);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_setupDataChannel(channel) {
|
|
133
|
+
this.dataChannel = channel;
|
|
134
|
+
channel.binaryType = 'arraybuffer';
|
|
135
|
+
|
|
136
|
+
channel.onopen = () => this.emit('channel-open');
|
|
137
|
+
channel.onclose = () => this.emit('channel-close');
|
|
138
|
+
channel.onerror = (err) => this.emit('error', err);
|
|
139
|
+
channel.onmessage = (event) => this.emit('data', event.data);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async _createOffer() {
|
|
143
|
+
this._createPeerConnection();
|
|
144
|
+
|
|
145
|
+
// Create data channel for streaming control + data
|
|
146
|
+
const dc = this.pc.createDataChannel('stream', {
|
|
147
|
+
ordered: true,
|
|
148
|
+
maxRetransmits: 3,
|
|
149
|
+
});
|
|
150
|
+
this._setupDataChannel(dc);
|
|
151
|
+
|
|
152
|
+
const offer = await this.pc.createOffer();
|
|
153
|
+
await this.pc.setLocalDescription(offer);
|
|
154
|
+
|
|
155
|
+
this.ws.send(JSON.stringify({
|
|
156
|
+
type: 'offer',
|
|
157
|
+
sdp: offer.sdp,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async _handleOffer(msg) {
|
|
162
|
+
this._createPeerConnection();
|
|
163
|
+
await this.pc.setRemoteDescription(new RTCSessionDescription({ type: 'offer', sdp: msg.sdp }));
|
|
164
|
+
const answer = await this.pc.createAnswer();
|
|
165
|
+
await this.pc.setLocalDescription(answer);
|
|
166
|
+
|
|
167
|
+
this.ws.send(JSON.stringify({
|
|
168
|
+
type: 'answer',
|
|
169
|
+
sdp: answer.sdp,
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async _handleAnswer(msg) {
|
|
174
|
+
await this.pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: msg.sdp }));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async _handleIceCandidate(msg) {
|
|
178
|
+
if (msg.candidate && this.pc) {
|
|
179
|
+
try { await this.pc.addIceCandidate(msg.candidate); } catch {}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Send a control message (JSON)
|
|
184
|
+
sendControl(msg) {
|
|
185
|
+
if (this.dataChannel?.readyState === 'open') {
|
|
186
|
+
this.dataChannel.send(JSON.stringify(msg));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Send binary chunk
|
|
191
|
+
sendChunk(buffer) {
|
|
192
|
+
if (this.dataChannel?.readyState === 'open') {
|
|
193
|
+
this.dataChannel.send(buffer);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
close() {
|
|
198
|
+
this._closed = true;
|
|
199
|
+
if (this.dataChannel) { try { this.dataChannel.close(); } catch {} }
|
|
200
|
+
if (this.pc) { try { this.pc.close(); } catch {} }
|
|
201
|
+
if (this.ws) {
|
|
202
|
+
try {
|
|
203
|
+
this.ws.send(JSON.stringify({ type: 'leave' }));
|
|
204
|
+
this.ws.close();
|
|
205
|
+
} catch {}
|
|
206
|
+
}
|
|
207
|
+
this.removeAllListeners();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Host side: serve media over WebRTC data channel
|
|
212
|
+
export class StreamHost extends StreamPeer {
|
|
213
|
+
constructor(opts) {
|
|
214
|
+
super({ ...opts, isInitiator: false });
|
|
215
|
+
this._fileHandler = null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
setFileHandler(handler) {
|
|
219
|
+
// handler(request) => { stream, size, mimeType } or null
|
|
220
|
+
this._fileHandler = handler;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async start() {
|
|
224
|
+
await this.connect();
|
|
225
|
+
|
|
226
|
+
this.on('data', async (raw) => {
|
|
227
|
+
let msg;
|
|
228
|
+
try { msg = JSON.parse(typeof raw === 'string' ? raw : new TextDecoder().decode(raw)); } catch { return; }
|
|
229
|
+
|
|
230
|
+
if (msg.type === 'media-library') {
|
|
231
|
+
this.sendControl({ type: 'media-library-response', ...await this._getMediaLibrary(msg.dir) });
|
|
232
|
+
} else if (msg.type === 'stream-request' && this._fileHandler) {
|
|
233
|
+
await this._streamFile(msg);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async _getMediaLibrary(dir) {
|
|
239
|
+
const fs = await import('fs');
|
|
240
|
+
const path = await import('path');
|
|
241
|
+
const os = await import('os');
|
|
242
|
+
|
|
243
|
+
const AUDIO = ['mp3', 'flac', 'wav', 'ogg', 'aac', 'm4a'];
|
|
244
|
+
const VIDEO = ['mp4', 'mkv', 'webm', 'avi', 'mov'];
|
|
245
|
+
const ALL = [...AUDIO, ...VIDEO];
|
|
246
|
+
|
|
247
|
+
let baseDir;
|
|
248
|
+
if (dir) {
|
|
249
|
+
const resolved = path.resolve(dir);
|
|
250
|
+
const musicDir = path.resolve(path.join(os.homedir(), 'Music'));
|
|
251
|
+
const videosDir = path.resolve(path.join(os.homedir(), 'Videos'));
|
|
252
|
+
const { listShares } = await import('./shares.js');
|
|
253
|
+
const shares = listShares({ allUsers: true });
|
|
254
|
+
const allowed = resolved === musicDir || resolved.startsWith(musicDir + path.sep)
|
|
255
|
+
|| resolved === videosDir || resolved.startsWith(videosDir + path.sep)
|
|
256
|
+
|| shares.some(s => s.sourcePath && (resolved === path.resolve(s.sourcePath) || resolved.startsWith(path.resolve(s.sourcePath) + path.sep)));
|
|
257
|
+
if (!allowed) {
|
|
258
|
+
return { files: [], baseDir: dir, error: 'Access denied' };
|
|
259
|
+
}
|
|
260
|
+
baseDir = resolved;
|
|
261
|
+
} else {
|
|
262
|
+
baseDir = path.join(os.homedir(), 'Music');
|
|
263
|
+
}
|
|
264
|
+
const results = [];
|
|
265
|
+
const MAX = 500;
|
|
266
|
+
|
|
267
|
+
function scan(d, depth) {
|
|
268
|
+
if (depth > 5 || results.length >= MAX) return;
|
|
269
|
+
let entries;
|
|
270
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
if (results.length >= MAX) return;
|
|
273
|
+
const full = path.join(d, entry.name);
|
|
274
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) scan(full, depth + 1);
|
|
275
|
+
else if (entry.isFile()) {
|
|
276
|
+
const ext = entry.name.split('.').pop().toLowerCase();
|
|
277
|
+
if (ALL.includes(ext)) {
|
|
278
|
+
let size = 0;
|
|
279
|
+
try { size = fs.statSync(full).size; } catch {}
|
|
280
|
+
results.push({ name: entry.name, path: full, size, type: AUDIO.includes(ext) ? 'audio' : 'video', ext });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
scan(baseDir, 0);
|
|
286
|
+
results.sort((a, b) => a.name.localeCompare(b.name));
|
|
287
|
+
return { files: results, baseDir };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async _streamFile(request) {
|
|
291
|
+
const fs = await import('fs');
|
|
292
|
+
const pathMod = await import('path');
|
|
293
|
+
const { filePath, rangeStart, rangeEnd } = request;
|
|
294
|
+
|
|
295
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
296
|
+
this.sendControl({ type: 'stream-error', error: 'Invalid path' });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const resolved = pathMod.resolve(filePath);
|
|
300
|
+
const { listShares } = await import('./shares.js');
|
|
301
|
+
const shares = listShares({ allUsers: true });
|
|
302
|
+
const allowed = shares.some(s => {
|
|
303
|
+
if (!s.sourcePath) return false;
|
|
304
|
+
const shareBase = pathMod.resolve(s.sourcePath);
|
|
305
|
+
return resolved === shareBase || resolved.startsWith(shareBase + pathMod.sep);
|
|
306
|
+
});
|
|
307
|
+
if (!allowed) {
|
|
308
|
+
this.sendControl({ type: 'stream-error', error: 'Access denied' });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let stat;
|
|
313
|
+
try { stat = fs.statSync(filePath); } catch {
|
|
314
|
+
this.sendControl({ type: 'stream-error', error: 'File not found' });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const start = rangeStart || 0;
|
|
319
|
+
const end = rangeEnd || stat.size - 1;
|
|
320
|
+
const CHUNK_SIZE = 16384; // 16KB chunks for data channel
|
|
321
|
+
|
|
322
|
+
this.sendControl({
|
|
323
|
+
type: 'stream-start',
|
|
324
|
+
size: stat.size,
|
|
325
|
+
rangeStart: start,
|
|
326
|
+
rangeEnd: end,
|
|
327
|
+
fileName: request.filePath.split(/[/\\]/).pop(),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const stream = fs.createReadStream(filePath, { start, end });
|
|
331
|
+
for await (const chunk of stream) {
|
|
332
|
+
// Wait if data channel is buffered
|
|
333
|
+
while (this.dataChannel && this.dataChannel.bufferedAmount > 1024 * 1024) {
|
|
334
|
+
await new Promise(r => setTimeout(r, 10));
|
|
335
|
+
}
|
|
336
|
+
if (this.dataChannel?.readyState !== 'open') break;
|
|
337
|
+
this.sendChunk(chunk);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this.sendControl({ type: 'stream-end' });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Client side: request and receive media over WebRTC
|
|
345
|
+
export class StreamClient extends StreamPeer {
|
|
346
|
+
constructor(opts) {
|
|
347
|
+
super({ ...opts, isInitiator: true });
|
|
348
|
+
this._chunks = [];
|
|
349
|
+
this._streamResolve = null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async requestMediaLibrary(dir) {
|
|
353
|
+
return new Promise((resolve) => {
|
|
354
|
+
const handler = (raw) => {
|
|
355
|
+
let msg;
|
|
356
|
+
try { msg = JSON.parse(typeof raw === 'string' ? raw : new TextDecoder().decode(raw)); } catch { return; }
|
|
357
|
+
if (msg.type === 'media-library-response') {
|
|
358
|
+
this.removeListener('data', handler);
|
|
359
|
+
resolve(msg);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
this.on('data', handler);
|
|
363
|
+
this.sendControl({ type: 'media-library', dir });
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async requestStream(filePath, { rangeStart, rangeEnd } = {}) {
|
|
368
|
+
return new Promise((resolve, reject) => {
|
|
369
|
+
let metadata = null;
|
|
370
|
+
const chunks = [];
|
|
371
|
+
|
|
372
|
+
const handler = (raw) => {
|
|
373
|
+
if (raw instanceof ArrayBuffer || Buffer.isBuffer(raw)) {
|
|
374
|
+
chunks.push(Buffer.isBuffer(raw) ? raw : Buffer.from(raw));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let msg;
|
|
379
|
+
try { msg = JSON.parse(typeof raw === 'string' ? raw : new TextDecoder().decode(raw)); } catch { return; }
|
|
380
|
+
|
|
381
|
+
if (msg.type === 'stream-start') {
|
|
382
|
+
metadata = msg;
|
|
383
|
+
} else if (msg.type === 'stream-end') {
|
|
384
|
+
this.removeListener('data', handler);
|
|
385
|
+
resolve({ metadata, data: Buffer.concat(chunks) });
|
|
386
|
+
} else if (msg.type === 'stream-error') {
|
|
387
|
+
this.removeListener('data', handler);
|
|
388
|
+
reject(new Error(msg.error));
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
this.on('data', handler);
|
|
393
|
+
this.sendControl({ type: 'stream-request', filePath, rangeStart, rangeEnd });
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import sodium from 'sodium-native';
|
|
2
|
+
|
|
3
|
+
export function encryptMessage(plaintext, senderPrivateKey, recipientPublicKey) {
|
|
4
|
+
const senderEd = Buffer.isBuffer(senderPrivateKey) ? senderPrivateKey : Buffer.from(senderPrivateKey, 'hex');
|
|
5
|
+
const recipientEd = Buffer.isBuffer(recipientPublicKey) ? recipientPublicKey : Buffer.from(recipientPublicKey, 'hex');
|
|
6
|
+
|
|
7
|
+
const senderCurve = Buffer.alloc(sodium.crypto_box_SECRETKEYBYTES);
|
|
8
|
+
const recipientCurve = Buffer.alloc(sodium.crypto_box_PUBLICKEYBYTES);
|
|
9
|
+
sodium.crypto_sign_ed25519_sk_to_curve25519(senderCurve, senderEd);
|
|
10
|
+
sodium.crypto_sign_ed25519_pk_to_curve25519(recipientCurve, recipientEd);
|
|
11
|
+
|
|
12
|
+
const message = Buffer.from(plaintext, 'utf8');
|
|
13
|
+
const nonce = Buffer.alloc(sodium.crypto_box_NONCEBYTES);
|
|
14
|
+
sodium.randombytes_buf(nonce);
|
|
15
|
+
|
|
16
|
+
const cipher = Buffer.alloc(message.length + sodium.crypto_box_MACBYTES);
|
|
17
|
+
sodium.crypto_box_easy(cipher, message, nonce, recipientCurve, senderCurve);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
nonce: nonce.toString('hex'),
|
|
21
|
+
cipher: cipher.toString('hex'),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function decryptMessage(encrypted, recipientPrivateKey, senderPublicKey) {
|
|
26
|
+
const recipientEd = Buffer.isBuffer(recipientPrivateKey) ? recipientPrivateKey : Buffer.from(recipientPrivateKey, 'hex');
|
|
27
|
+
const senderEd = Buffer.isBuffer(senderPublicKey) ? senderPublicKey : Buffer.from(senderPublicKey, 'hex');
|
|
28
|
+
|
|
29
|
+
const recipientCurve = Buffer.alloc(sodium.crypto_box_SECRETKEYBYTES);
|
|
30
|
+
const senderCurve = Buffer.alloc(sodium.crypto_box_PUBLICKEYBYTES);
|
|
31
|
+
sodium.crypto_sign_ed25519_sk_to_curve25519(recipientCurve, recipientEd);
|
|
32
|
+
sodium.crypto_sign_ed25519_pk_to_curve25519(senderCurve, senderEd);
|
|
33
|
+
|
|
34
|
+
const nonce = Buffer.from(encrypted.nonce, 'hex');
|
|
35
|
+
const cipher = Buffer.from(encrypted.cipher, 'hex');
|
|
36
|
+
|
|
37
|
+
const message = Buffer.alloc(cipher.length - sodium.crypto_box_MACBYTES);
|
|
38
|
+
const ok = sodium.crypto_box_open_easy(message, cipher, nonce, senderCurve, recipientCurve);
|
|
39
|
+
if (!ok) throw new Error('Decryption failed - message tampered or wrong key');
|
|
40
|
+
|
|
41
|
+
return message.toString('utf8');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function signMessage(message, privateKey) {
|
|
45
|
+
const sk = Buffer.isBuffer(privateKey) ? privateKey : Buffer.from(privateKey, 'hex');
|
|
46
|
+
const msg = Buffer.from(message, 'utf8');
|
|
47
|
+
const signature = Buffer.alloc(sodium.crypto_sign_BYTES);
|
|
48
|
+
sodium.crypto_sign_detached(signature, msg, sk);
|
|
49
|
+
return signature.toString('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function verifySignature(message, signature, publicKey) {
|
|
53
|
+
const pk = Buffer.isBuffer(publicKey) ? publicKey : Buffer.from(publicKey, 'hex');
|
|
54
|
+
const msg = Buffer.from(message, 'utf8');
|
|
55
|
+
const sig = Buffer.from(signature, 'hex');
|
|
56
|
+
return sodium.crypto_sign_verify_detached(sig, msg, pk);
|
|
57
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import sodium from 'sodium-native';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import config from '../utils/config.js';
|
|
5
|
+
|
|
6
|
+
const CHUNK_SIZE = 65536; // 64KB chunks for streaming encryption
|
|
7
|
+
|
|
8
|
+
export function generateShareKey() {
|
|
9
|
+
const key = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES);
|
|
10
|
+
sodium.randombytes_buf(key);
|
|
11
|
+
return key;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getEncryptedShareDir(shareId) {
|
|
15
|
+
const baseDir = path.dirname(config.path);
|
|
16
|
+
return path.join(baseDir, 'encrypted-shares', shareId);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getDecryptedOutputDir(shareName) {
|
|
20
|
+
const baseDir = path.dirname(config.path);
|
|
21
|
+
return path.join(baseDir, 'downloads', shareName);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Encrypt a single file using secretstream. Output format:
|
|
25
|
+
// [24-byte header][{4-byte LE chunk_len}{encrypted_chunk}...]
|
|
26
|
+
export function encryptFile(inputPath, outputPath, shareKey) {
|
|
27
|
+
const state = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_STATEBYTES);
|
|
28
|
+
const header = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES);
|
|
29
|
+
sodium.crypto_secretstream_xchacha20poly1305_init_push(state, header, shareKey);
|
|
30
|
+
|
|
31
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
32
|
+
const input = fs.openSync(inputPath, 'r');
|
|
33
|
+
const output = fs.openSync(outputPath, 'w');
|
|
34
|
+
fs.writeSync(output, header);
|
|
35
|
+
|
|
36
|
+
const plainBuf = Buffer.alloc(CHUNK_SIZE);
|
|
37
|
+
let bytesRead;
|
|
38
|
+
const fileSize = fs.fstatSync(input).size;
|
|
39
|
+
let totalRead = 0;
|
|
40
|
+
|
|
41
|
+
while (true) {
|
|
42
|
+
bytesRead = fs.readSync(input, plainBuf, 0, CHUNK_SIZE, null);
|
|
43
|
+
if (bytesRead === 0) break;
|
|
44
|
+
totalRead += bytesRead;
|
|
45
|
+
const isLast = totalRead >= fileSize;
|
|
46
|
+
const plain = plainBuf.subarray(0, bytesRead);
|
|
47
|
+
const cipher = Buffer.alloc(bytesRead + sodium.crypto_secretstream_xchacha20poly1305_ABYTES);
|
|
48
|
+
const tag = isLast
|
|
49
|
+
? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
|
|
50
|
+
: sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
|
51
|
+
sodium.crypto_secretstream_xchacha20poly1305_push(state, cipher, plain, null, tag);
|
|
52
|
+
const lenBuf = Buffer.alloc(4);
|
|
53
|
+
lenBuf.writeUInt32LE(cipher.length, 0);
|
|
54
|
+
fs.writeSync(output, lenBuf);
|
|
55
|
+
fs.writeSync(output, cipher);
|
|
56
|
+
if (isLast) break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fs.closeSync(input);
|
|
60
|
+
fs.closeSync(output);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Decrypt a single file encrypted by encryptFile
|
|
64
|
+
export function decryptFile(inputPath, outputPath, shareKey) {
|
|
65
|
+
const state = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_STATEBYTES);
|
|
66
|
+
const header = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES);
|
|
67
|
+
|
|
68
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
69
|
+
const input = fs.openSync(inputPath, 'r');
|
|
70
|
+
const tmpPath = outputPath + '.tmp';
|
|
71
|
+
const output = fs.openSync(tmpPath, 'w');
|
|
72
|
+
|
|
73
|
+
fs.readSync(input, header, 0, header.length, null);
|
|
74
|
+
// sodium-native v5: init_pull returns void, throws are surfaced by pull()
|
|
75
|
+
sodium.crypto_secretstream_xchacha20poly1305_init_pull(state, header, shareKey);
|
|
76
|
+
|
|
77
|
+
const lenBuf = Buffer.alloc(4);
|
|
78
|
+
let fileOffset = header.length;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
while (true) {
|
|
82
|
+
const readLen = fs.readSync(input, lenBuf, 0, 4, fileOffset);
|
|
83
|
+
if (readLen < 4) break;
|
|
84
|
+
fileOffset += 4;
|
|
85
|
+
const chunkLen = lenBuf.readUInt32LE(0);
|
|
86
|
+
const maxChunk = CHUNK_SIZE + sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
|
|
87
|
+
if (chunkLen === 0 || chunkLen > maxChunk + 64) {
|
|
88
|
+
throw new Error('Corrupt ciphertext: invalid chunk length');
|
|
89
|
+
}
|
|
90
|
+
const cipher = Buffer.alloc(chunkLen);
|
|
91
|
+
fs.readSync(input, cipher, 0, chunkLen, fileOffset);
|
|
92
|
+
fileOffset += chunkLen;
|
|
93
|
+
const plain = Buffer.alloc(chunkLen - sodium.crypto_secretstream_xchacha20poly1305_ABYTES);
|
|
94
|
+
const tag = Buffer.alloc(1);
|
|
95
|
+
// sodium-native v5: pull throws on failure instead of returning -1
|
|
96
|
+
sodium.crypto_secretstream_xchacha20poly1305_pull(state, plain, tag, cipher, null);
|
|
97
|
+
fs.writeSync(output, plain);
|
|
98
|
+
if (tag[0] === sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) break;
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
fs.closeSync(input);
|
|
102
|
+
fs.closeSync(output);
|
|
103
|
+
try { fs.unlinkSync(tmpPath); } catch {}
|
|
104
|
+
throw new Error('Decryption failed — data corrupted or wrong key');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fs.closeSync(input);
|
|
108
|
+
fs.closeSync(output);
|
|
109
|
+
fs.renameSync(tmpPath, outputPath);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Encrypt entire directory tree recursively into destDir
|
|
113
|
+
// Includes per-file timeout (60s default) to prevent hangs on platforms with sodium-native issues (e.g. WSL)
|
|
114
|
+
export function encryptDirectory(sourcePath, destDir, shareKey, { timeoutMs = 60000 } = {}) {
|
|
115
|
+
const results = [];
|
|
116
|
+
function encryptWithTimeout(inputPath, outputPath) {
|
|
117
|
+
const start = Date.now();
|
|
118
|
+
encryptFile(inputPath, outputPath, shareKey);
|
|
119
|
+
if (Date.now() - start > timeoutMs) {
|
|
120
|
+
throw new Error(`Encryption timed out for ${path.basename(inputPath)} (>${timeoutMs / 1000}s). This may indicate a platform issue with sodium-native.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function walk(dir, relBase) {
|
|
124
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
const fullPath = path.join(dir, entry.name);
|
|
127
|
+
const rel = relBase ? path.join(relBase, entry.name) : entry.name;
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
walk(fullPath, rel);
|
|
130
|
+
} else if (entry.isFile()) {
|
|
131
|
+
const encPath = path.join(destDir, rel + '.pal');
|
|
132
|
+
encryptWithTimeout(fullPath, encPath);
|
|
133
|
+
results.push({ original: rel, encrypted: rel + '.pal' });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (fs.statSync(sourcePath).isDirectory()) {
|
|
138
|
+
walk(sourcePath, '');
|
|
139
|
+
} else {
|
|
140
|
+
const name = path.basename(sourcePath) + '.pal';
|
|
141
|
+
const encPath = path.join(destDir, name);
|
|
142
|
+
encryptWithTimeout(sourcePath, encPath);
|
|
143
|
+
results.push({ original: path.basename(sourcePath), encrypted: name });
|
|
144
|
+
}
|
|
145
|
+
return results;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Decrypt all .pal files in encryptedDir into destDir
|
|
149
|
+
export function decryptDirectory(encryptedDir, destDir, shareKey) {
|
|
150
|
+
function walk(dir, relBase) {
|
|
151
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const fullPath = path.join(dir, entry.name);
|
|
154
|
+
const rel = relBase ? path.join(relBase, entry.name) : entry.name;
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
walk(fullPath, rel);
|
|
157
|
+
} else if (entry.isFile() && entry.name.endsWith('.pal')) {
|
|
158
|
+
const originalName = rel.slice(0, -4); // remove .pal
|
|
159
|
+
const outPath = path.join(destDir, originalName);
|
|
160
|
+
decryptFile(fullPath, outPath, shareKey);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
walk(encryptedDir, '');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Wrap shareKey for a recipient using crypto_box_seal (anonymous ECIES)
|
|
168
|
+
// recipientPublicKeyHex: Ed25519 public key (will be converted to Curve25519)
|
|
169
|
+
export function encryptShareKeyForRecipient(shareKey, recipientPublicKeyHex) {
|
|
170
|
+
const ed25519Pk = Buffer.from(recipientPublicKeyHex, 'hex');
|
|
171
|
+
const curve25519Pk = Buffer.alloc(sodium.crypto_box_PUBLICKEYBYTES);
|
|
172
|
+
sodium.crypto_sign_ed25519_pk_to_curve25519(curve25519Pk, ed25519Pk);
|
|
173
|
+
|
|
174
|
+
const ciphertext = Buffer.alloc(shareKey.length + sodium.crypto_box_SEALBYTES);
|
|
175
|
+
sodium.crypto_box_seal(ciphertext, shareKey, curve25519Pk);
|
|
176
|
+
return ciphertext.toString('hex');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Unwrap shareKey using own Ed25519 keypair (converted to Curve25519)
|
|
180
|
+
export function decryptShareKey(encryptedShareKeyHex, myPublicKeyHex, myPrivateKeyHex) {
|
|
181
|
+
const ed25519Pk = Buffer.from(myPublicKeyHex, 'hex');
|
|
182
|
+
const ed25519Sk = Buffer.from(myPrivateKeyHex, 'hex');
|
|
183
|
+
if (ed25519Pk.length !== sodium.crypto_sign_PUBLICKEYBYTES) throw new Error('Invalid public key length');
|
|
184
|
+
if (ed25519Sk.length !== sodium.crypto_sign_SECRETKEYBYTES) throw new Error('Invalid private key length');
|
|
185
|
+
const curve25519Pk = Buffer.alloc(sodium.crypto_box_PUBLICKEYBYTES);
|
|
186
|
+
const curve25519Sk = Buffer.alloc(sodium.crypto_box_SECRETKEYBYTES);
|
|
187
|
+
sodium.crypto_sign_ed25519_pk_to_curve25519(curve25519Pk, ed25519Pk);
|
|
188
|
+
sodium.crypto_sign_ed25519_sk_to_curve25519(curve25519Sk, ed25519Sk);
|
|
189
|
+
|
|
190
|
+
const ciphertext = Buffer.from(encryptedShareKeyHex, 'hex');
|
|
191
|
+
const shareKey = Buffer.alloc(ciphertext.length - sodium.crypto_box_SEALBYTES);
|
|
192
|
+
const ok = sodium.crypto_box_seal_open(shareKey, ciphertext, curve25519Pk, curve25519Sk);
|
|
193
|
+
if (!ok) throw new Error('Failed to decrypt share key — wrong identity or corrupted data');
|
|
194
|
+
return shareKey;
|
|
195
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import sodium from 'sodium-native';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
|
|
4
|
+
const OPSLIMIT = sodium.crypto_pwhash_OPSLIMIT_SENSITIVE;
|
|
5
|
+
const MEMLIMIT = sodium.crypto_pwhash_MEMLIMIT_SENSITIVE;
|
|
6
|
+
const ALG = sodium.crypto_pwhash_ALG_ARGON2ID13;
|
|
7
|
+
|
|
8
|
+
export function hashSharePassword(password) {
|
|
9
|
+
const salt = Buffer.alloc(sodium.crypto_pwhash_SALTBYTES);
|
|
10
|
+
sodium.randombytes_buf(salt);
|
|
11
|
+
|
|
12
|
+
const key = Buffer.alloc(32);
|
|
13
|
+
sodium.crypto_pwhash(key, Buffer.from(password, 'utf8'), salt, OPSLIMIT, MEMLIMIT, ALG);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
salt: salt.toString('hex'),
|
|
17
|
+
hash: key.toString('hex'),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function verifySharePassword(password, salt, hash) {
|
|
22
|
+
const saltBuf = Buffer.from(salt, 'hex');
|
|
23
|
+
const key = Buffer.alloc(32);
|
|
24
|
+
sodium.crypto_pwhash(key, Buffer.from(password, 'utf8'), saltBuf, OPSLIMIT, MEMLIMIT, ALG);
|
|
25
|
+
const hashBuf = Buffer.from(hash, 'hex');
|
|
26
|
+
if (key.length !== hashBuf.length) return false;
|
|
27
|
+
return crypto.timingSafeEqual(key, hashBuf);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function deriveKeyFromPassword(password, salt) {
|
|
31
|
+
const saltBuf = Buffer.isBuffer(salt) ? salt : Buffer.from(salt, 'hex');
|
|
32
|
+
const key = Buffer.alloc(32);
|
|
33
|
+
sodium.crypto_pwhash(key, Buffer.from(password, 'utf8'), saltBuf, OPSLIMIT, MEMLIMIT, ALG);
|
|
34
|
+
return key;
|
|
35
|
+
}
|