/** * 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); }