Files
internxt-webdav/src/download.js
elpatron 406e31f338 Namensentschlüsselung, Phase 4 PUT, Bridge-Fix, Debug-Tools
- 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
2026-02-28 11:49:26 +01:00

134 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}