- name-decrypt.js: AES-Entschlüsselung für Datei-/Ordnernamen (CRYPTO_SECRET2) - path-resolver.js: getPlainName für alle Pfad-Operationen - upload.js: PUT mit Verschlüsselung, Bridge API v2 - download.js: Bridge 400-Fix (x-api-version, Header) - debug-name-decrypt.js: Test-Skript für Namensentschlüsselung - docs: CRYPTO_SECRET/CRYPTO_SECRET2 dokumentiert Made-with: Cursor
134 lines
4.5 KiB
JavaScript
134 lines
4.5 KiB
JavaScript
/**
|
||
* Internxt Datei-Download – Bridge API + Entschlüsselung
|
||
*/
|
||
|
||
import crypto from 'crypto';
|
||
import { mnemonicToSeed } from 'bip39';
|
||
|
||
const BRIDGE_URL = process.env.BRIDGE_URL || process.env.STORJ_BRIDGE || 'https://gateway.internxt.com/network';
|
||
|
||
async function sha256Hex(data) {
|
||
return crypto.createHash('sha256').update(data).digest('hex');
|
||
}
|
||
|
||
async function sha512Combined(key, data) {
|
||
return crypto.createHash('sha512').update(key).update(data).digest('hex');
|
||
}
|
||
|
||
async function getFileDeterministicKey(key, data) {
|
||
const hashHex = await sha512Combined(key, data);
|
||
return Buffer.from(hashHex, 'hex');
|
||
}
|
||
|
||
async function generateFileBucketKey(mnemonic, bucketId) {
|
||
const seed = await mnemonicToSeed(mnemonic);
|
||
return getFileDeterministicKey(seed, Buffer.from(bucketId, 'hex'));
|
||
}
|
||
|
||
export async function generateFileKey(mnemonic, bucketId, index) {
|
||
const bucketKey = await generateFileBucketKey(mnemonic, bucketId);
|
||
return (await getFileDeterministicKey(bucketKey.subarray(0, 32), index)).subarray(0, 32);
|
||
}
|
||
|
||
async function getAuth(bridgeUser, bridgePass) {
|
||
const password = await sha256Hex(bridgePass);
|
||
const encoded = Buffer.from(`${bridgeUser}:${password}`).toString('base64');
|
||
return `Basic ${encoded}`;
|
||
}
|
||
|
||
const BRIDGE_HEADERS = {
|
||
'x-api-version': '2',
|
||
'content-type': 'application/json; charset=utf-8',
|
||
'internxt-version': '1.0',
|
||
'internxt-client': 'drive-web',
|
||
};
|
||
|
||
async function getFileInfo(bucketId, fileId, authHeader, opts = {}) {
|
||
const url = `${BRIDGE_URL}/buckets/${bucketId}/files/${fileId}/info`;
|
||
const res = await fetch(url, {
|
||
headers: { Authorization: authHeader, ...BRIDGE_HEADERS },
|
||
...opts,
|
||
});
|
||
if (!res.ok) {
|
||
let detail = '';
|
||
try {
|
||
const text = await res.text();
|
||
if (text) detail = ` – ${text.slice(0, 200)}`;
|
||
} catch (_) {}
|
||
throw new Error(`Bridge file info: ${res.status}${detail}`);
|
||
}
|
||
return res.json();
|
||
}
|
||
|
||
/** Holt Download-URLs: nutzt shards aus fileInfo (API v2) oder fallback auf mirrors-Endpoint */
|
||
async function getDownloadUrls(bucketId, fileId, authHeader, fileInfo, opts = {}) {
|
||
if (fileInfo?.shards?.length) {
|
||
return fileInfo.shards.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
||
}
|
||
const mirrors = [];
|
||
let skip = 0;
|
||
const limit = 3;
|
||
while (true) {
|
||
const res = await fetch(
|
||
`${BRIDGE_URL}/buckets/${bucketId}/files/${fileId}?limit=${limit}&skip=${skip}`,
|
||
{ headers: { Authorization: authHeader, ...BRIDGE_HEADERS }, ...opts }
|
||
);
|
||
if (!res.ok) throw new Error(`Bridge mirrors: ${res.status}`);
|
||
const results = await res.json();
|
||
const valid = results
|
||
.filter((m) => m.farmer?.nodeID && m.farmer?.port && m.farmer?.address)
|
||
.filter((m) => !m.parity)
|
||
.sort((a, b) => a.index - b.index);
|
||
valid.forEach((r) => mirrors.push(r));
|
||
if (results.length === 0) break;
|
||
skip += limit;
|
||
}
|
||
return mirrors;
|
||
}
|
||
|
||
/**
|
||
* Lädt eine Datei herunter und gibt einen ReadableStream zurück.
|
||
* @param {{ bucketId: string, fileId: string, bridgeUser: string, bridgePass: string, mnemonic: string, signal?: AbortSignal }} params
|
||
*/
|
||
export async function downloadFileStream(params) {
|
||
const { bucketId, fileId, bridgeUser, bridgePass, mnemonic, signal } = params;
|
||
const fetchOpts = signal ? { signal } : {};
|
||
|
||
const authHeader = await getAuth(bridgeUser, bridgePass);
|
||
const fileMeta = await getFileInfo(bucketId, fileId, authHeader, fetchOpts);
|
||
const downloadItems = await getDownloadUrls(bucketId, fileId, authHeader, fileMeta, fetchOpts);
|
||
|
||
const index = Buffer.from(fileMeta.index, 'hex');
|
||
const iv = index.slice(0, 16);
|
||
const key = await generateFileKey(mnemonic, bucketId, index);
|
||
const decipher = crypto.createDecipheriv('aes-256-ctr', key, iv);
|
||
|
||
const sortedItems = downloadItems.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
||
const { Readable } = await import('stream');
|
||
|
||
const combined = new Readable({
|
||
async read() {
|
||
if (this._fetched) return;
|
||
this._fetched = true;
|
||
try {
|
||
for (const item of sortedItems) {
|
||
const res = await fetch(item.url, fetchOpts);
|
||
if (!res.ok) throw new Error(`Download shard: ${res.status}`);
|
||
if (!res.body) throw new Error('No content received');
|
||
const reader = res.body.getReader();
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
this.push(value);
|
||
}
|
||
}
|
||
this.push(null);
|
||
} catch (err) {
|
||
this.destroy(err);
|
||
}
|
||
},
|
||
});
|
||
|
||
return combined.pipe(decipher);
|
||
}
|