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
This commit is contained in:
2026-02-28 11:49:26 +01:00
parent 43b814d984
commit 406e31f338
13 changed files with 932 additions and 83 deletions

133
src/download.js Normal file
View File

@@ -0,0 +1,133 @@
/**
* 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);
}