From 406e31f33829c993eb5045b5330a70d0091d6a55 Mon Sep 17 00:00:00 2001 From: elpatron Date: Sat, 28 Feb 2026 11:49:26 +0100 Subject: [PATCH] =?UTF-8?q?Namensentschl=C3=BCsselung,=20Phase=204=20PUT,?= =?UTF-8?q?=20Bridge-Fix,=20Debug-Tools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .env.example | 4 + docs/browser-token-auth.md | 23 ++- docs/webdav-architektur.md | 8 +- package-lock.json | 68 +++------ package.json | 2 + src/bridge-test.js | 106 ++++++++++++++ src/debug-files.js | 70 +++++++++ src/debug-name-decrypt.js | 91 ++++++++++++ src/download.js | 133 +++++++++++++++++ src/name-decrypt.js | 37 +++++ src/path-resolver.js | 69 +++++++-- src/server.js | 292 ++++++++++++++++++++++++++++++++++--- src/upload.js | 112 ++++++++++++++ 13 files changed, 932 insertions(+), 83 deletions(-) create mode 100644 src/bridge-test.js create mode 100644 src/debug-files.js create mode 100644 src/debug-name-decrypt.js create mode 100644 src/download.js create mode 100644 src/name-decrypt.js create mode 100644 src/upload.js diff --git a/.env.example b/.env.example index 910899b..dc4ea0f 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,13 @@ # Internxt API (Production - aus internxt/cli .env.template) DRIVE_API_URL=https://gateway.internxt.com/drive +# Bridge/Network für Datei-Up/Download (optional, Default: gateway.internxt.com/network) +# BRIDGE_URL=https://gateway.internxt.com/network # Crypto secret - CLI: 6KYQBP847D4ATSFA # drive-web nutzt REACT_APP_CRYPTO_SECRET (evtl. anderer Wert - aus drive.internxt.com JS extrahieren) CRYPTO_SECRET=6KYQBP847D4ATSFA +# Für Namensentschlüsselung (CRYPTO_SECRET2). Falls nicht gesetzt, wird CRYPTO_SECRET verwendet. +# CRYPTO_SECRET2=6KYQBP847D4ATSFA # DEBUG=1 # Salt-Decryption testen (ob CRYPTO_SECRET stimmt) diff --git a/docs/browser-token-auth.md b/docs/browser-token-auth.md index b3bf2f0..b5d46a1 100644 --- a/docs/browser-token-auth.md +++ b/docs/browser-token-auth.md @@ -51,6 +51,9 @@ console.log('Mnemonic:', localStorage.getItem('xMnemonic') || '(nicht gefunden)' ``` INXT_TOKEN=eyJhbGciOiJIUzI1NiIs... INXT_MNEMONIC=word1 word2 word3 ... +# Namensentschlüsselung: CRYPTO_SECRET oder CRYPTO_SECRET2 (CLI-Default: 6KYQBP847D4ATSFA) +# Falls Datei-/Ordnernamen verschlüsselt angezeigt werden: CRYPTO_SECRET2 aus drive.internxt.com extrahieren +CRYPTO_SECRET=6KYQBP847D4ATSFA ``` ## WebDAV-Server starten @@ -59,10 +62,28 @@ INXT_MNEMONIC=word1 word2 word3 ... npm start ``` -Server läuft auf `http://127.0.0.1:3005`. Phase 1+2 aktiv: PROPFIND, MKCOL (Ordner anlegen), DELETE, MOVE. Windows Explorer: Netzlaufwerk verbinden → `http://127.0.0.1:3005`. +Server läuft auf `http://127.0.0.1:3005`. Phase 1–4 aktiv: PROPFIND, MKCOL, DELETE, MOVE, GET, PUT. Für GET und PUT wird INXT_MNEMONIC benötigt. + +### PowerShell Copy-Item: „Null character in path“ + +Windows/.NET fügt bei WebDAV-Pfaden manchmal Null-Bytes ein. **Workaround:** + +```powershell +# Variante 1: Direkt per HTTP (umgeht WebDAV-Bugs, UUID aus dir i: übernehmen) +Invoke-WebRequest -Uri "http://127.0.0.1:3005/_.69942103-e16f-4714-89bb-9f9f7d3b1bd5" -OutFile test.md + +# Upload per PUT (PowerShell) +Invoke-WebRequest -Uri "http://127.0.0.1:3005/meine-datei.txt" -Method PUT -Body "Inhalt" -ContentType "application/octet-stream" + +# Variante 2: Robocopy (kopiert alle Dateien aus Root) +robocopy "i:\" "." /NFL /NDL + +# Variante 3: Explorer – Datei per Drag & Drop kopieren +``` Windows Explorer: Netzlaufwerk verbinden → `http://127.0.0.1:3005`. ## Hinweise +- **Bridge-API**: Der Download nutzt die Internxt Bridge mit `x-api-version: 2` und den Headern `internxt-version`/`internxt-client`. Ohne diese liefert die Bridge 400. - **Token-Ablauf**: Tokens laufen nach einiger Zeit ab (typisch Stunden). Bei 401-Fehlern erneut einloggen und Token aktualisieren. - **Sicherheit**: Mnemonic und Token sind hochsensibel. Nicht in Git committen, `.env` in `.gitignore` belassen. - **Nur für Sie**: Die Tokens sind an Ihre Session gebunden. Für andere Nutzer funktioniert dieser Ansatz nicht. diff --git a/docs/webdav-architektur.md b/docs/webdav-architektur.md index 26a6fcf..0c97134 100644 --- a/docs/webdav-architektur.md +++ b/docs/webdav-architektur.md @@ -52,12 +52,12 @@ Die drive-web nutzt `Network.client` (Bridge) und `NetworkFacade` für Up-/Downl 1. **Phase 1:** PROPFIND (Verzeichnis auflisten) – ✅ implementiert 2. **Phase 2:** MKCOL, DELETE, MOVE – ✅ implementiert -3. **Phase 3:** GET (Download) – Bridge + Entschlüsselung -4. **Phase 4:** PUT (Upload) – Verschlüsselung + Bridge +3. **Phase 3:** GET (Download) – Bridge + Entschlüsselung – ✅ implementiert +4. **Phase 4:** PUT (Upload) – Verschlüsselung + Bridge – ✅ implementiert -### Hinweis: Ordnernamen +### Namensentschlüsselung -Internxt nutzt Zero-Knowledge-Verschlüsselung. Die API liefert verschlüsselte Namen (`name`). Die Pfadauflösung funktioniert, weil WebDAV-URLs diese Namen enthalten. Eine spätere Phase kann Namensentschlüsselung mit Mnemonic ergänzen (drive-web Crypto-Logik). +Internxt nutzt Zero-Knowledge-Verschlüsselung. Die API liefert verschlüsselte Namen (`name`). Wenn `plain_name` fehlt, kann der Server mit `CRYPTO_SECRET2` (oder `CRYPTO_SECRET`) Namen entschlüsseln – analog zur drive-web `aes.decrypt`-Logik mit `secret2-parentId`/`secret2-folderId`. Ohne gesetztes Secret werden die rohen (verschlüsselten) Namen verwendet. ## Token vs. Bridge-Credentials diff --git a/package-lock.json b/package-lock.json index 434fd61..d58b4bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,25 +8,15 @@ "name": "internxt-webdav", "version": "0.1.0", "dependencies": { - "@dashlane/pqc-kem-kyber512-node": "^1.0.0", "@internxt/lib": "^1.4.1", "@internxt/sdk": "^1.13.2", + "bip39": "^3.1.0", "crypto-js": "^4.1.1", "dotenv": "^16.0.0", "express": "^4.18.0", - "openpgp": "^5.11.3", "webdav-server": "^2.6.0" } }, - "node_modules/@dashlane/pqc-kem-kyber512-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@dashlane/pqc-kem-kyber512-node/-/pqc-kem-kyber512-node-1.0.0.tgz", - "integrity": "sha512-gVzQwP/1OqKLyYZ/oRI9uECSnYIcLUcZbnAA34Q2l8X1eXq5JWf304tDp1UTdYdJ+ZE58SmQ68VCa/WvpCviGw==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/@internxt/lib": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@internxt/lib/-/lib-1.4.1.tgz", @@ -130,18 +120,6 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, - "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -159,11 +137,26 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "license": "MIT" + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "license": "ISC", + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, + "node_modules/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/body-parser": { "version": "1.20.4", @@ -833,12 +826,6 @@ "node": ">= 0.6" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "license": "ISC" - }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -878,19 +865,6 @@ "node": ">= 0.8" } }, - "node_modules/openpgp": { - "version": "5.11.3", - "resolved": "https://registry.npmjs.org/openpgp/-/openpgp-5.11.3.tgz", - "integrity": "sha512-jXOPfIteBUQ2zSmRG4+Y6PNntIIDEAvoM/lOYCnvpXAByJEruzrHQZWE/0CGOKHbubwUuty2HoPHsqBzyKHOpA==", - "deprecated": "This version is deprecated and will no longer receive security patches. Please refer to https://github.com/openpgpjs/openpgpjs/wiki/Updating-from-previous-versions for details on how to upgrade to a newer supported version.", - "license": "LGPL-3.0+", - "dependencies": { - "asn1.js": "^5.0.0" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", diff --git a/package.json b/package.json index 8a84a72..5347ae4 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,13 @@ "scripts": { "auth-test": "node src/auth-poc.js", "token-test": "node src/token-test.js", + "debug-names": "node src/debug-name-decrypt.js", "start": "node src/server.js" }, "dependencies": { "@internxt/lib": "^1.4.1", "@internxt/sdk": "^1.13.2", + "bip39": "^3.1.0", "crypto-js": "^4.1.1", "dotenv": "^16.0.0", "express": "^4.18.0", diff --git a/src/bridge-test.js b/src/bridge-test.js new file mode 100644 index 0000000..0783093 --- /dev/null +++ b/src/bridge-test.js @@ -0,0 +1,106 @@ +/** + * Bridge-Test: Ruft die Bridge API direkt auf + * Hilft beim Debuggen von 400/401 Fehlern + * + * Aufruf: node src/bridge-test.js + */ + +import 'dotenv/config'; +import crypto from 'crypto'; +import { Users } from '@internxt/sdk/dist/drive/index.js'; + +const BRIDGE_URL = process.env.BRIDGE_URL || process.env.STORJ_BRIDGE || 'https://gateway.internxt.com/network'; +const DRIVE_API_URL = process.env.DRIVE_API_URL || 'https://gateway.internxt.com/drive'; +const token = process.env.INXT_TOKEN; + +async function sha256Hex(data) { + return crypto.createHash('sha256').update(String(data)).digest('hex'); +} + +async function main() { + if (!token) { + console.error('INXT_TOKEN fehlt'); + process.exit(1); + } + + const users = Users.client(DRIVE_API_URL, { clientName: 'drive-web', clientVersion: '1.0' }, { + token, + unauthorizedCallback: () => { + throw new Error('Token ungültig'); + }, + }); + + const refresh = await users.refreshUser(); + const user = refresh?.user ?? refresh; + const bridgeUser = user?.bridgeUser || user?.email; + const bridgePass = user?.userId; + + console.log('User:', user?.email); + console.log('User bucket:', user?.bucket); + console.log('File bucket (from debug):', '695cd596fe2b60d4ec9c3f4e'); + console.log('Buckets gleich?', user?.bucket === '695cd596fe2b60d4ec9c3f4e'); + console.log('User (alle Keys):', Object.keys(user || {})); + console.log('bridgeUser:', bridgeUser ? bridgeUser.slice(0, 8) + '…' : '(fehlt)'); + console.log('bridgePass (userId):', bridgePass ? '***' : '(fehlt)'); + console.log(''); + + if (!bridgeUser || !bridgePass) { + console.error('Bridge-Credentials fehlen (bridgeUser oder userId)'); + process.exit(1); + } + + // Aus debug-files: bucket=695cd596fe2b60d4ec9c3f4e, fileId=69a2bf36fe5cd3b21d81deb7 + const bucketId = '695cd596fe2b60d4ec9c3f4e'; + const fileId = '69a2bf36fe5cd3b21d81deb7'; + + const password = await sha256Hex(bridgePass); + const auth = Buffer.from(`${bridgeUser}:${password}`).toString('base64'); + + const url = `${BRIDGE_URL}/buckets/${bucketId}/files/${fileId}/info`; + console.log('Request:', url); + console.log(''); + + // Test 1: Full SDK-Headers (Basic Auth + x-api-version + internxt-*) + console.log('--- Test 1: Full SDK-Headers ---'); + const sdkHeaders = { + Authorization: `Basic ${auth}`, + 'x-api-version': '2', + 'content-type': 'application/json; charset=utf-8', + 'internxt-version': '1.0', + 'internxt-client': 'drive-web', + }; + const res = await fetch(url, { headers: sdkHeaders }); + + console.log('Status:', res.status, res.statusText); + let text = await res.text(); + if (text) { + try { + console.log('Body:', JSON.stringify(JSON.parse(text), null, 2)); + } catch { + console.log('Body:', text.slice(0, 500)); + } + } + + // Test 2: x-token (wie drive-web manchmal) + if (res.status !== 200) { + console.log(''); + console.log('--- Test 2: x-token Header ---'); + const res2 = await fetch(url, { + headers: { 'x-token': token }, + }); + console.log('Status:', res2.status, res2.statusText); + text = await res2.text(); + if (text) { + try { + console.log('Body:', JSON.stringify(JSON.parse(text), null, 2)); + } catch { + console.log('Body:', text.slice(0, 500)); + } + } + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/src/debug-files.js b/src/debug-files.js new file mode 100644 index 0000000..b101fe9 --- /dev/null +++ b/src/debug-files.js @@ -0,0 +1,70 @@ +/** + * Debug: Zeigt die Struktur der Dateien aus der Drive API + * Hilft beim Debuggen von Bridge 400 (bucket/fileId Format) + * + * Aufruf: node src/debug-files.js [Pfad] + * Beispiel: node src/debug-files.js / + */ + +import 'dotenv/config'; +import { Storage, Users } from '@internxt/sdk/dist/drive/index.js'; +import { resolveFolder } from './path-resolver.js'; + +const DRIVE_API_URL = process.env.DRIVE_API_URL || 'https://gateway.internxt.com/drive'; +const token = process.env.INXT_TOKEN; + +if (!token) { + console.error('INXT_TOKEN fehlt'); + process.exit(1); +} + +const appDetails = { clientName: 'drive-web', clientVersion: '1.0' }; +const apiSecurity = { + token, + unauthorizedCallback: () => { + throw new Error('Token ungültig'); + }, +}; + +async function main() { + const path = process.argv[2] || '/'; + const storage = Storage.client(DRIVE_API_URL, appDetails, apiSecurity); + const users = Users.client(DRIVE_API_URL, appDetails, apiSecurity); + + const refresh = await users.refreshUser(); + const user = refresh?.user ?? refresh; + const rootUuid = user?.rootFolderUuid || user?.rootFolderId; + + if (!rootUuid) { + console.error('Root-Ordner nicht gefunden'); + process.exit(1); + } + + const folder = await resolveFolder(storage, rootUuid, path); + if (!folder) { + console.error('Pfad nicht gefunden:', path); + process.exit(1); + } + + const [contentPromise] = storage.getFolderContentByUuid({ folderUuid: folder.uuid }); + const content = await contentPromise; + const files = content?.files || []; + + console.log('=== Dateien in', path, '(rohe API-Antwort) ===\n'); + for (const f of files) { + console.log('Name:', f.plain_name || f.name || f.plainName); + console.log(' uuid:', f.uuid); + console.log(' bucket:', f.bucket, '(Typ:', typeof f.bucket, ')'); + console.log(' bucket_id:', f.bucket_id); + console.log(' fileId:', f.fileId, '(Typ:', typeof f.fileId, ', Länge:', f.fileId?.length, ')'); + console.log(' file_id:', f.file_id); + console.log(' networkFileId:', f.networkFileId); + console.log(' size:', f.size); + console.log(''); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/src/debug-name-decrypt.js b/src/debug-name-decrypt.js new file mode 100644 index 0000000..f18d4b2 --- /dev/null +++ b/src/debug-name-decrypt.js @@ -0,0 +1,91 @@ +/** + * Debug: Testet Namensentschlüsselung + * Aufruf: node src/debug-name-decrypt.js [Pfad] + * + * Zeigt für jede Datei/Ordner: verschlüsselter Name → entschlüsselter Name + * Hilft zu prüfen, ob CRYPTO_SECRET2 korrekt ist. + */ + +import 'dotenv/config'; +import { Storage, Users } from '@internxt/sdk/dist/drive/index.js'; +import { getPlainName } from './name-decrypt.js'; +import { resolveFolder } from './path-resolver.js'; + +const DRIVE_API_URL = process.env.DRIVE_API_URL || 'https://gateway.internxt.com/drive'; +const token = process.env.INXT_TOKEN; + +if (!token) { + console.error('INXT_TOKEN fehlt'); + process.exit(1); +} + +const secret = process.env.CRYPTO_SECRET2 || process.env.CRYPTO_SECRET; +if (!secret) { + console.error('CRYPTO_SECRET oder CRYPTO_SECRET2 fehlt in .env'); + process.exit(1); +} + +const appDetails = { clientName: 'drive-web', clientVersion: '1.0' }; +const apiSecurity = { + token, + unauthorizedCallback: () => { + throw new Error('Token ungültig'); + }, +}; + +async function main() { + const path = process.argv[2] || '/'; + const storage = Storage.client(DRIVE_API_URL, appDetails, apiSecurity); + const users = Users.client(DRIVE_API_URL, appDetails, apiSecurity); + + const refresh = await users.refreshUser(); + const user = refresh?.user ?? refresh; + const rootUuid = user?.rootFolderUuid || user?.rootFolderId; + + if (!rootUuid) { + console.error('Root-Ordner nicht gefunden'); + process.exit(1); + } + + const folder = await resolveFolder(storage, rootUuid, path); + if (!folder) { + console.error('Pfad nicht gefunden:', path); + process.exit(1); + } + + const [contentPromise] = storage.getFolderContentByUuid({ folderUuid: folder.uuid }); + const content = await contentPromise; + const folders = content?.children || []; + const files = content?.files || []; + + console.log('=== Namensentschlüsselung in', path, '==='); + console.log('CRYPTO_SECRET2/CRYPTO_SECRET:', secret ? secret.substring(0, 4) + '***' : '(nicht gesetzt)'); + console.log(''); + + for (const c of folders) { + const plain = getPlainName(c.name, c.plain_name ?? c.plainName, c.parent_id ?? c.parentId, null); + const ok = plain !== c.name && plain.length > 0 && !/^[A-Za-z0-9+/=]{20,}$/.test(plain); + console.log('Ordner:', ok ? '✓' : '✗', plain); + console.log(' verschlüsselt:', c.name?.substring(0, 50) + '...'); + console.log(' parent_id:', c.parent_id ?? c.parentId); + console.log(''); + } + + for (const f of files) { + const plain = getPlainName(f.name, f.plain_name ?? f.plainName, null, f.folder_id ?? f.folderId); + const ok = plain !== f.name && plain.length > 0 && !/^[A-Za-z0-9+/=]{20,}$/.test(plain); + console.log('Datei:', ok ? '✓' : '✗', plain); + console.log(' verschlüsselt:', f.name?.substring(0, 50) + '...'); + console.log(' folder_id:', f.folder_id ?? f.folderId); + console.log(''); + } + + if (folders.length === 0 && files.length === 0) { + console.log('(Leerer Ordner)'); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/src/download.js b/src/download.js new file mode 100644 index 0000000..8d4024d --- /dev/null +++ b/src/download.js @@ -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); +} diff --git a/src/name-decrypt.js b/src/name-decrypt.js new file mode 100644 index 0000000..c442243 --- /dev/null +++ b/src/name-decrypt.js @@ -0,0 +1,37 @@ +/** + * Entschlüsselung von Datei- und Ordnernamen (Zero-Knowledge) + * Nutzt @internxt/lib aes.decrypt mit CRYPTO_SECRET2 + parentId/folderId + * Siehe drive-web: app/crypto/services/utils.ts getItemPlainName + */ + +import { aes } from '@internxt/lib'; + +const CRYPTO_SECRET2 = process.env.CRYPTO_SECRET2 || process.env.CRYPTO_SECRET || ''; + +/** + * Entschlüsselt einen Namen, falls nötig. + * @param {string} encryptedName - Verschlüsselter Name (base64) + * @param {string} plainName - Bereits entschlüsselter Name (von API) + * @param {number} [parentId] - Für Ordner: parent_id + * @param {number} [folderId] - Für Dateien: folder_id + * @returns {string} - Plain-Name + */ +export function getPlainName(encryptedName, plainName, parentId, folderId) { + if (plainName != null && String(plainName).trim().length > 0) { + return String(plainName).trim(); + } + if (!encryptedName) return 'Unbenannt'; + if (!CRYPTO_SECRET2) return encryptedName; + try { + const key = parentId != null + ? `${CRYPTO_SECRET2}-${parentId}` + : folderId != null + ? `${CRYPTO_SECRET2}-${folderId}` + : null; + if (!key) return encryptedName; + const decrypted = aes.decrypt(encryptedName, key); + return decrypted && String(decrypted).trim() ? String(decrypted).trim() : encryptedName; + } catch (_) { + return encryptedName; + } +} diff --git a/src/path-resolver.js b/src/path-resolver.js index 17009af..d10b198 100644 --- a/src/path-resolver.js +++ b/src/path-resolver.js @@ -1,7 +1,16 @@ /** * WebDAV-Pfad ↔ Internxt-UUID Auflösung + * Mit optionaler Namensentschlüsselung (CRYPTO_SECRET2) */ +import { getPlainName } from './name-decrypt.js'; + +/** Entfernt Null-Bytes und Steuerzeichen (für Windows-Kompatibilität) */ +function sanitize(s) { + if (s == null) return ''; + return String(s).replace(/\0/g, '').replace(/[\x00-\x1f\x7f]/g, ''); +} + /** * Normalisiert WebDAV-Pfad zu Segmenten (ohne führendes /). * "/" -> [], "/folder1" -> ["folder1"], "/folder1/sub" -> ["folder1","sub"] @@ -32,9 +41,10 @@ export async function resolveFolder(storage, rootFolderUuid, path) { for (const segment of segments) { const [contentPromise] = storage.getFolderContentByUuid({ folderUuid: currentUuid }); const content = await contentPromise; - const child = content?.children?.find( - (c) => (c.plain_name || c.name || '').toLowerCase() === segment.toLowerCase() - ); + const child = content?.children?.find((c) => { + const name = getPlainName(c.name, c.plain_name ?? c.plainName, c.parent_id ?? c.parentId, null); + return sanitize(name).toLowerCase() === sanitize(segment).toLowerCase(); + }); if (!child) return null; currentUuid = child.uuid; } @@ -59,11 +69,11 @@ export async function listFolder(storage, rootFolderUuid, path) { const folders = (content?.children || []).map((c) => ({ uuid: c.uuid, - name: c.plain_name || c.name || 'Unbenannt', + name: getPlainName(c.name, c.plain_name ?? c.plainName, c.parent_id ?? c.parentId, null) || 'Unbenannt', })); const files = (content?.files || []).map((f) => ({ uuid: f.uuid, - name: f.plain_name || f.name || f.plainName || 'Unbenannt', + name: getPlainName(f.name, f.plain_name ?? f.plainName, null, f.folder_id ?? f.folderId) || 'Unbenannt', size: f.size || 0, updatedAt: f.updatedAt || f.modificationTime || f.creationTime, })); @@ -90,18 +100,49 @@ export async function resolveResource(storage, rootFolderUuid, path) { const [contentPromise] = storage.getFolderContentByUuid({ folderUuid: parent.uuid }); const content = await contentPromise; - const folder = content?.children?.find( - (c) => (c.plain_name || c.name || '').toLowerCase() === childName.toLowerCase() - ); - if (folder) { - return { uuid: folder.uuid, type: 'folder', name: folder.plain_name || folder.name, parentUuid: parent.uuid }; + const uuidMatch = childName.match(/^_.([a-f0-9-]{36})$/i); + if (uuidMatch) { + const file = content?.files?.find((f) => f.uuid === uuidMatch[1]); + if (file) { + const bucket = file.bucket ?? file.bucket_id; + const fileId = file.fileId ?? file.file_id ?? file.networkFileId; + const name = getPlainName(file.name, file.plain_name ?? file.plainName, null, file.folder_id ?? file.folderId); + return { + uuid: file.uuid, + type: 'file', + name, + parentUuid: parent.uuid, + bucket, + fileId, + }; + } } - const file = content?.files?.find( - (f) => (f.plain_name || f.name || f.plainName || '').toLowerCase() === childName.toLowerCase() - ); + const folder = content?.children?.find((c) => { + const name = getPlainName(c.name, c.plain_name ?? c.plainName, c.parent_id ?? c.parentId, null); + return sanitize(name).toLowerCase() === sanitize(childName).toLowerCase(); + }); + if (folder) { + const name = getPlainName(folder.name, folder.plain_name ?? folder.plainName, folder.parent_id ?? folder.parentId, null); + return { uuid: folder.uuid, type: 'folder', name, parentUuid: parent.uuid }; + } + + const file = content?.files?.find((f) => { + const name = getPlainName(f.name, f.plain_name ?? f.plainName, null, f.folder_id ?? f.folderId); + return sanitize(name).toLowerCase() === sanitize(childName).toLowerCase(); + }); if (file) { - return { uuid: file.uuid, type: 'file', name: file.plain_name || file.name || file.plainName, parentUuid: parent.uuid }; + const bucket = file.bucket ?? file.bucket_id; + const fileId = file.fileId ?? file.file_id ?? file.networkFileId; + const name = getPlainName(file.name, file.plain_name ?? file.plainName, null, file.folder_id ?? file.folderId); + return { + uuid: file.uuid, + type: 'file', + name, + parentUuid: parent.uuid, + bucket, + fileId, + }; } return null; diff --git a/src/server.js b/src/server.js index 20611fc..a93e84b 100644 --- a/src/server.js +++ b/src/server.js @@ -1,5 +1,5 @@ /** - * WebDAV-Server für Internxt Drive (Phase 1+2: PROPFIND, MKCOL, DELETE, MOVE) + * WebDAV-Server für Internxt Drive (Phase 1–4: PROPFIND, MKCOL, DELETE, MOVE, GET, PUT) * * Nutzt Browser-Token (INXT_TOKEN, INXT_MNEMONIC) aus .env. * Siehe docs/browser-token-auth.md @@ -9,9 +9,12 @@ import 'dotenv/config'; import express from 'express'; import { createClients, refreshUser } from './internxt-client.js'; import { pathToSegments, segmentsToPath, listFolder, resolveFolder, resolveResource } from './path-resolver.js'; +import { downloadFileStream } from './download.js'; +import { uploadFileBuffer } from './upload.js'; const PORT = parseInt(process.env.PORT || '3005', 10); const token = process.env.INXT_TOKEN; +const mnemonic = process.env.INXT_MNEMONIC; if (!token) { console.error('Fehler: INXT_TOKEN muss gesetzt sein. Siehe docs/browser-token-auth.md'); @@ -20,7 +23,8 @@ if (!token) { const app = express(); -// Request-Body für PROPFIND +// Request-Body: PUT als Raw (Datei-Upload), PROPFIND als Text +app.use(express.raw({ type: (req) => req.method === 'PUT', limit: '1gb' })); app.use(express.text({ type: 'application/xml', limit: '1kb' })); /** @@ -32,12 +36,34 @@ function toLastModified(iso) { return d.toUTCString(); } +/** Pfad ohne Query-String aus Request extrahieren */ +function getPathFromRequest(req) { + const url = req.url || '/'; + const path = url.split('?')[0]; + try { + return decodeURIComponent(path); + } catch (_) { + return path; + } +} + +/** + * Entfernt Null-Bytes und Steuerzeichen (verursacht "Null character in path" unter Windows). + * Behält druckbare Zeichen: Space (0x20) bis ~ (0x7e), inkl. + = / für Base64. + */ +function sanitizeForPath(s) { + if (s == null) return ''; + return String(s) + .replace(/\0/g, '') + .replace(/[\x00-\x1f\x7f]/g, ''); +} + /** * Escaped XML-Text */ function escapeXml(s) { if (s == null) return ''; - return String(s) + return sanitizeForPath(String(s)) .replace(/&/g, '&') .replace(//g, '>') @@ -51,7 +77,16 @@ function escapeXml(s) { function buildPropfindResponse(baseUrl, items) { const ns = 'DAV:'; const responses = items.map((item) => { - const href = baseUrl + (item.path === '/' ? '/' : item.path + (item.isCollection ? '/' : '')); + const safePath = sanitizeForPath(item.path); + const pathForHref = + safePath === '/' + ? '/' + : safePath + .split('/') + .filter(Boolean) + .map((seg) => encodeURIComponent(seg)) + .join('/') + (item.isCollection ? '/' : ''); + const href = baseUrl + (pathForHref === '/' ? '/' : '/' + pathForHref); const lastMod = toLastModified(item.updatedAt); const resourcetype = item.isCollection ? `` @@ -83,12 +118,10 @@ function buildPropfindResponse(baseUrl, items) { */ async function handlePropfind(req, res) { const depth = req.headers['depth'] || '1'; - let path = req.url || '/'; - try { - path = decodeURIComponent(path); - } catch (_) {} + let path = getPathFromRequest(req); if (!path.startsWith('/')) path = '/' + path; if (path !== '/' && path.endsWith('/')) path = path.slice(0, -1); + path = sanitizeForPath(path); const baseUrl = `${req.protocol}://${req.get('host')}`; @@ -124,28 +157,34 @@ async function handlePropfind(req, res) { // Kinder bei depth 1 if (depth !== '0') { for (const f of listing.folders) { - const childPath = path === '/' ? '/' + f.name : path + '/' + f.name; + const safeName = sanitizeForPath(f.name) || 'Unbenannt'; + const childPath = path === '/' ? '/' + safeName : path + '/' + safeName; items.push({ path: childPath, - name: f.name, + name: safeName, isCollection: true, updatedAt: null, size: null, }); } for (const f of listing.files) { - const childPath = path === '/' ? '/' + f.name : path + '/' + f.name; + const rawName = sanitizeForPath(f.name) || 'Unbenannt'; + const useUuidPath = /[+=]/.test(rawName) || rawName.length > 80; + const pathSegment = useUuidPath ? `_.${f.uuid}` : rawName; + const childPath = path === '/' ? '/' + pathSegment : path + '/' + pathSegment; items.push({ path: childPath, - name: f.name, + name: useUuidPath ? f.uuid : rawName, isCollection: false, updatedAt: f.updatedAt, size: f.size, + uuid: f.uuid, }); } } - const xml = buildPropfindResponse(baseUrl, items); + let xml = buildPropfindResponse(baseUrl, items); + xml = xml.replace(/\0/g, ''); // Sicherheitsnetz: keine Null-Bytes in der Antwort res.set('Content-Type', 'application/xml; charset="utf-8"'); res.status(207).send(xml); } catch (err) { @@ -349,6 +388,212 @@ async function handleMove(req, res) { } } +/** + * GET Handler – Datei herunterladen + */ +async function handleGet(req, res) { + let path = req.url || '/'; + try { + path = decodeURIComponent(path); + } catch (_) {} + if (!path.startsWith('/')) path = '/' + path; + if (path.endsWith('/')) path = path.slice(0, -1); + if (path === '/') { + res.status(405).send('Verzeichnis kann nicht heruntergeladen werden'); + return; + } + + if (!mnemonic) { + res.status(500).send('INXT_MNEMONIC fehlt für Datei-Entschlüsselung'); + return; + } + + try { + const { storage, rootUuid } = await getContext(); + const resource = await resolveResource(storage, rootUuid, path); + if (!resource) { + res.status(404).send('Nicht gefunden'); + return; + } + if (resource.type !== 'file') { + res.status(405).send('Keine Datei'); + return; + } + if (!resource.bucket || !resource.fileId) { + res.status(404).send('Datei hat keinen Inhalt (leere Datei)'); + return; + } + + const refresh = await refreshUser(token); + const user = refresh.user; + const bridgeUser = user?.bridgeUser || user?.email; + const bridgePass = user?.userId; + + if (!bridgeUser || !bridgePass) { + res.status(500).send('Bridge-Credentials fehlen'); + return; + } + + const stream = await downloadFileStream({ + bucketId: resource.bucket, + fileId: resource.fileId, + bridgeUser, + bridgePass, + mnemonic, + }); + + res.set('Content-Disposition', `attachment; filename="${encodeURIComponent(resource.name)}"`); + stream.pipe(res); + stream.on('error', (err) => { + if (!res.headersSent) res.status(500).send(err.message); + else res.destroy(); + }); + } catch (err) { + console.error('GET Fehler:', err.message); + if (err.message?.includes('Token') || err.response?.status === 401) { + res.status(401).send('Nicht autorisiert'); + return; + } + if (!res.headersSent) res.status(500).send(err.message || 'Interner Fehler'); + } +} + +/** + * HEAD Handler – wie GET, aber nur Header + */ +async function handleHead(req, res) { + let path = req.url || '/'; + try { + path = decodeURIComponent(path); + } catch (_) {} + if (!path.startsWith('/')) path = '/' + path; + if (path.endsWith('/')) path = path.slice(0, -1); + if (path === '/') { + res.status(405).send(); + return; + } + + try { + const { storage, rootUuid } = await getContext(); + const resource = await resolveResource(storage, rootUuid, path); + if (!resource) { + res.status(404).send(); + return; + } + if (resource.type !== 'file') { + res.status(405).send(); + return; + } + res.set('Content-Disposition', `attachment; filename="${encodeURIComponent(resource.name)}"`); + res.status(200).send(); + } catch (err) { + if (!res.headersSent) res.status(500).send(); + } +} + +/** Parst Dateiname in plainName + Typ (Extension) */ +function parseFileName(name) { + if (!name || typeof name !== 'string') return { plainName: 'Unbenannt', type: '' }; + const s = sanitizeForPath(name); + const lastDot = s.lastIndexOf('.'); + if (lastDot <= 0) return { plainName: s || 'Unbenannt', type: '' }; + return { + plainName: s.slice(0, lastDot) || 'Unbenannt', + type: s.slice(lastDot + 1).toLowerCase() || '', + }; +} + +/** + * PUT Handler – Datei hochladen + */ +async function handlePut(req, res) { + let path = getPathFromRequest(req); + if (!path.startsWith('/')) path = '/' + path; + if (path.endsWith('/')) path = path.slice(0, -1); + path = sanitizeForPath(path); + + if (path === '/') { + res.status(403).send('Root kann nicht überschrieben werden'); + return; + } + + const buffer = req.body; + if (!Buffer.isBuffer(buffer)) { + res.status(400).send('Kein Dateiinhalt erhalten'); + return; + } + + if (!mnemonic) { + res.status(500).send('INXT_MNEMONIC fehlt für Datei-Verschlüsselung'); + return; + } + + try { + const { storage, rootUuid } = await getContext(); + const segments = pathToSegments(path); + const parentPath = segmentsToPath(segments.slice(0, -1)); + const fileName = segments[segments.length - 1]; + + const parent = await resolveFolder(storage, rootUuid, parentPath); + if (!parent) { + res.status(409).send('Zielordner existiert nicht'); + return; + } + + const existing = await resolveResource(storage, rootUuid, path); + if (existing) { + if (existing.type === 'file') { + await storage.deleteFileByUuid(existing.uuid); + } else { + res.status(409).send('Ziel ist ein Ordner'); + return; + } + } + + const refresh = await refreshUser(token); + const user = refresh.user; + const bridgeUser = user?.bridgeUser || user?.email; + const bridgePass = user?.userId; + const bucketId = user?.bucket; + + if (!bridgeUser || !bridgePass || !bucketId) { + res.status(500).send('Bridge-Credentials oder Bucket fehlen'); + return; + } + + const { plainName, type } = parseFileName(fileName); + const fileId = await uploadFileBuffer({ + bucketId, + bridgeUser, + bridgePass, + mnemonic, + buffer, + }); + + const date = new Date().toISOString(); + await storage.createFileEntryByUuid({ + fileId, + type: type || 'bin', + size: buffer.length, + plainName, + bucket: bucketId, + folderUuid: parent.uuid, + encryptVersion: '03-aes', + modificationTime: date, + date, + }); + + res.status(201).send(); + } catch (err) { + console.error('PUT Fehler:', err.message); + if (err.message?.includes('Token') || err.response?.status === 401) { + res.status(401).send('Nicht autorisiert'); + return; + } + if (!res.headersSent) res.status(500).send(err.message || 'Interner Fehler'); + } +} + // WebDAV Endpoints app.options('*', (req, res) => { res.set('DAV', '1, 2'); @@ -362,12 +607,25 @@ app.use((req, res, next) => { handlePropfind(req, res); return; } - if (req.method === 'GET' || req.method === 'HEAD') { - res.status(501).send('GET (Download) noch nicht implementiert – Phase 3'); + if (req.method === 'GET') { + handleGet(req, res).catch((err) => { + console.error('GET unhandled:', err); + if (!res.headersSent) res.status(500).send(err.message); + }); + return; + } + if (req.method === 'HEAD') { + handleHead(req, res).catch((err) => { + console.error('HEAD unhandled:', err); + if (!res.headersSent) res.status(500).send(err.message); + }); return; } if (req.method === 'PUT') { - res.status(501).send('PUT (Upload) noch nicht implementiert – Phase 4'); + handlePut(req, res).catch((err) => { + console.error('PUT unhandled:', err); + if (!res.headersSent) res.status(500).send(err.message); + }); return; } if (req.method === 'DELETE') { @@ -396,6 +654,6 @@ app.use((req, res, next) => { app.listen(PORT, () => { console.log(`Internxt WebDAV Server – http://127.0.0.1:${PORT}`); - console.log('Phase 1+2: PROPFIND, MKCOL, DELETE, MOVE aktiv.'); + console.log('Phase 1–4: PROPFIND, MKCOL, DELETE, MOVE, GET, PUT aktiv.'); console.log('Verwendung: z.B. Windows Explorer → Netzlaufwerk verbinden'); }); diff --git a/src/upload.js b/src/upload.js new file mode 100644 index 0000000..9e9f2c3 --- /dev/null +++ b/src/upload.js @@ -0,0 +1,112 @@ +/** + * Internxt Datei-Upload – Bridge API + Verschlüsselung + * Nutzt @internxt/sdk Network + uploadFile + */ + +import crypto from 'crypto'; +import { validateMnemonic } from 'bip39'; +import { Network } from '@internxt/sdk/dist/network/index.js'; +import { uploadFile as sdkUploadFile } from '@internxt/sdk/dist/network/upload.js'; +import { generateFileKey } from './download.js'; + +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 ripemd160FromHex(dataHex) { + return crypto.createHash('ripemd160').update(Buffer.from(dataHex, 'hex')).digest('hex'); +} + +async function getAuth(bridgeUser, bridgePass) { + const password = await sha256Hex(bridgePass); + return { username: bridgeUser, password }; +} + +async function getEncryptedBuffer(plainBuffer, cipher) { + const encrypted = Buffer.concat([cipher.update(plainBuffer), cipher.final()]); + const sha256HexDigest = crypto.createHash('sha256').update(encrypted).digest('hex'); + const hash = await ripemd160FromHex(sha256HexDigest); + return [encrypted, hash]; +} + +async function uploadToUrl(buffer, url, signal) { + const res = await fetch(url, { + method: 'PUT', + headers: { 'content-type': 'application/octet-stream' }, + body: buffer, + signal, + }); + if (!res.ok) throw new Error(`Upload fehlgeschlagen: ${res.status}`); +} + +/** + * Lädt eine Datei hoch und gibt die Bridge fileId zurück. + * @param {{ + * bucketId: string, + * bridgeUser: string, + * bridgePass: string, + * mnemonic: string, + * buffer: Buffer, + * signal?: AbortSignal + * }} params + * @returns {Promise} fileId + */ +export async function uploadFileBuffer(params) { + const { bucketId, bridgeUser, bridgePass, mnemonic, buffer, signal } = params; + const fileSize = buffer.length; + + if (fileSize === 0) { + throw new Error('Leere Datei kann nicht hochgeladen werden'); + } + + if (!validateMnemonic(mnemonic)) { + throw new Error('Ungültiges Mnemonic'); + } + + const auth = await getAuth(bridgeUser, bridgePass); + const network = Network.client(BRIDGE_URL, { + clientName: 'drive-web', + clientVersion: '1.0', + }, { + bridgeUser: auth.username, + userId: auth.password, + }); + + let encryptedBuffer = null; + let contentHash = null; + + const encryptFile = async (_algorithm, key, iv) => { + const cipher = crypto.createCipheriv('aes-256-ctr', key, iv); + [encryptedBuffer, contentHash] = await getEncryptedBuffer(buffer, cipher); + }; + + const uploadToBridge = async (url) => { + let abortSignal; + if (signal) { + const ctrl = new AbortController(); + signal.addEventListener('abort', () => ctrl.abort()); + abortSignal = ctrl.signal; + } + await uploadToUrl(encryptedBuffer, url, abortSignal); + return contentHash; + }; + + const fileId = await sdkUploadFile( + network, + { + validateMnemonic, + randomBytes: (n) => crypto.randomBytes(n), + generateFileKey: (m, b, i) => generateFileKey(m, b, i), + algorithm: { type: 'aes-256-ctr', ivSize: 32 }, + }, + bucketId, + mnemonic, + fileSize, + encryptFile, + uploadToBridge + ); + + return fileId; +}