libsodium crypto_secretstream_xchacha20poly1305_init_pull throws fails to initlailize state despite correct key and header [closed]

3 weeks ago 13
ARTICLE AD BOX

I'm implementing end-to-end encryption for large files using libsodium.js with crypto_secretstream_xchacha20poly1305. The encryption works fine, but decryption fails with "State konnte nicht initialisiert werden" (State could not be initialized).

When trying to initialize the decryption state with crypto_secretstream_xchacha20poly1305_init_pull(), it fails even though:

The key is 32 bytes (correct length for crypto_secretstream_xchacha20poly1305_KEYBYTES)

The header is 24 bytes (correct length for crypto_secretstream_xchacha20poly1305_HEADERBYTES)

Both key and header are passed as Uint8Array

Encryptor: Crypto Header Uint8Array(24) [ 119, 236, 242, 100, 144, 128, 29, 94, 227, 167, … ] Generierter Key (Base64URL): dZqmIY8RjO_flrBXm71jbg5VeZfl5EYhZNICo5qw0fM ed:17:21 Starte Entschlüsselung... CryptoHeader: Decryptor: CryptoHeader: Uint8Array(24) [ 119, 236, 242, 100, 144, 128, 29, 94, 227, 167, … ] <anonymous code>:1:147461 Decryptor: Key bytes: Uint8Array(32) [ 117, 154, 166, 33, 143, 17, 140, 239, 223, 150, … ] Decryptor: Header length: 24 ed:207:25 Decryptor: Key length: 32 Decryptor init error: Error: State konnte nicht initialisiert werden init /ed:218 decryptAll /ed:258 <anonymous> /ed:444 async* /ed:420 <anonymous code>:1:147461 decryptAll error: Error: State konnte nicht initialisiert werden init /ed:218 decryptAll /ed:258 <anonymous> /ed:444 async* /ed:420 <anonymous code>:1:147461 Uncaught (in promise) Error: State konnte nicht initialisiert werden

My minimal viable example:

<!DOCTYPE html> <html lang="de"> <head> <meta charset="UTF-8"> <title>Datei Encrypt/Decrypt Test</title> </head> <body> <h1>Datei Encrypt/Decrypt Test</h1> <input type="file" id="fileInput" multiple> <button id="startButton">Start Verschlüsselung/Entschlüsselung</button> <div id="output"></div> <script> const log = (msg) => { console.log(msg); const outDiv = document.getElementById("output"); outDiv.innerHTML += msg + "<br>"; }; const DEBUG_ENCRYPTOR = true; // Setze auf false, wenn kein Logging class Encryptor { sodium = null; key = null; state = null; cryptoHeader = null; encoder = new TextEncoder(); CHUNK_SIZE = 64 * 1024; // 64 KB, kann auf 1MB hochgesetzt werden constructor(keyString = null) { this.userKeyString = keyString; } async init() { this.sodium = await SodiumLoader.getInstance(); if (DEBUG_ENCRYPTOR) console.log("Encryptor: Zufälliger Key erzeugt"); this.key = this.sodium.randombytes_buf(this.sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES); const result = this.sodium.crypto_secretstream_xchacha20poly1305_init_push(this.key); this.state = result.state; this.cryptoHeader = result.header; if (DEBUG_ENCRYPTOR) console.log("Encryptor: Crypto-State initialisiert"); } __encryptChunk(data) { return this.sodium.crypto_secretstream_xchacha20poly1305_push( this.state, data, null, this.sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE ); } async __streamFile(file, outputChunks, progressCallback, offsetStart, totalSize) { if (DEBUG_ENCRYPTOR) console.log(`Encryptor: Verschlüssele Datei ${file.name}, Größe: ${file.size}`); const reader = file.stream().getReader(); let leftover = new Uint8Array(0); let processed = 0; while (true) { const { value, done } = await reader.read(); if (done) break; let buffer = new Uint8Array(leftover.length + value.length); buffer.set(leftover, 0); buffer.set(value, leftover.length); let offset = 0; while (offset + this.CHUNK_SIZE <= buffer.length) { const chunk = buffer.slice(offset, offset + this.CHUNK_SIZE); const [frameLen, frameData] = this.__encryptChunk(chunk); // <- variable umbenannt outputChunks.push(frameLen, frameData); offset += this.CHUNK_SIZE; processed += this.CHUNK_SIZE; if (progressCallback) progressCallback((offsetStart + processed) / totalSize); if (DEBUG_ENCRYPTOR) console.log(`Encryptor: Datei ${file.name} Fortschritt ${(processed / file.size * 100).toFixed(1)}%`); } leftover = buffer.slice(offset); } if (leftover.length > 0) { const [frameLen, frameData] = this.__encryptChunk(leftover); // <- variable umbenannt outputChunks.push(frameLen, frameData); processed += leftover.length; if (progressCallback) progressCallback(offsetStart + processed / totalSize); if (DEBUG_ENCRYPTOR) console.log(`Encryptor: Datei ${file.name} abgeschlossen`); } } async encryptFiles(fileRefs, progressCallback = null) { if (!this.state) await this.init(); const totalSize = fileRefs.reduce((sum, f) => sum + f.size, 0); if (DEBUG_ENCRYPTOR) console.log(`Encryptor: Verschlüsselung gestartet, Gesamtgröße: ${totalSize}`); const outputChunks = []; // Crypto-Header UNVERSCHLÜSSELT an den Anfang outputChunks.push(this.cryptoHeader); if (DEBUG_ENCRYPTOR) console.warn("Encryptor: Crypto Header ", this.cryptoHeader); // Magic Bytes verschlüsseln const [magicLen, magicFrame] = this.__encryptChunk(this.encoder.encode("MYCNT1")); outputChunks.push(magicLen, magicFrame); // Header JSON verschlüsseln const headerBytes = this.encoder.encode(JSON.stringify({ files: fileRefs.map(f => ({ name: f.name, size: f.size })) })); const headerLengthBytes = new Uint8Array(4); new DataView(headerLengthBytes.buffer).setUint32(0, headerBytes.length, false); const [headerLenFrame, headerLenData] = this.__encryptChunk(headerLengthBytes); outputChunks.push(headerLenFrame, headerLenData); const [headerJsonFrameLen, headerJsonFrame] = this.__encryptChunk(headerBytes); outputChunks.push(headerJsonFrameLen, headerJsonFrame); // Dateien verschlüsseln let offsetStart = 0; for (const file of fileRefs) { await this.__streamFile(file, outputChunks, progressCallback, offsetStart, totalSize); offsetStart += file.size; } if (DEBUG_ENCRYPTOR) console.log("Encryptor: Verschlüsselung abgeschlossen, Blob erzeugen"); // Blob / File erzeugen const result = new File(outputChunks, 'container.mycnt', { type: 'application/octet-stream' }); return result; } getKey() { return this.key; } getKeyB64() { return btoa(String.fromCharCode(...this.key)); } getKeyB64URL() { return sodium.to_base64( this.key, sodium.base64_variants.URLSAFE_NO_PADDING ); } } const DEBUG_DECRYPTOR = true; // Setze auf false, um Logs auszuschalten class Decryptor { sodium = null; key = null; state = null; cryptoHeader = null; decoder = new TextDecoder(); reader = null; leftover = new Uint8Array(0); constructor(file, keyInput) { this.payload = file; this.keyInput = keyInput; // nur speichern, NICHT dekodieren } async init() { try { this.sodium = await SodiumLoader.getInstance(); if (typeof this.keyInput === "string") { this.key = this.sodium.from_base64( this.keyInput, this.sodium.base64_variants.URLSAFE_NO_PADDING ); } else if (this.keyInput instanceof Uint8Array) { this.key = this.keyInput; } else { throw new Error("Key muss Base64URL-String oder Uint8Array sein"); } const expectedKeyLength = this.sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES; if (this.key.length !== expectedKeyLength) { throw new Error(`Falsche Key-Länge: ${this.key.length} Bytes (erwarte ${expectedKeyLength})`); } // Header direkt aus File lesen const headerSize = this.sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES; const arrayBuffer = await this.payload.slice(0, headerSize).arrayBuffer(); this.cryptoHeader = new Uint8Array(arrayBuffer); if (DEBUG_DECRYPTOR) console.warn("Decryptor: CryptoHeader: ", this.cryptoHeader); // Reader starten this.reader = this.payload.slice(headerSize).stream().getReader(); if (DEBUG_DECRYPTOR) { console.log("Decryptor: Key bytes:", this.key); console.log("Decryptor: Header length:", this.cryptoHeader.length); console.log("Decryptor: Key length:", this.key.length); } // State initialisieren const initResult = this.sodium.crypto_secretstream_xchacha20poly1305_init_pull( this.cryptoHeader, this.key ); if (!initResult || !initResult.state) { throw new Error("State konnte nicht initialisiert werden"); } this.state = initResult.state; if (DEBUG_DECRYPTOR) console.log("Decryptor: Erfolgreich initialisiert"); } catch (error) { console.error("Decryptor init error:", error); throw error; } } async readNextFrame() { const { value, done } = await this.reader.read(); if (done) return null; return value; } async __decryptChunk(chunk) { if (!chunk || chunk.length === 0) return null; try { if (!this.state) throw new Error("State nicht initialisiert"); const result = this.sodium.crypto_secretstream_xchacha20poly1305_pull(this.state, chunk); i

Key length is exactly 32 bytes

Header length is exactly 24 bytes

Key encoding/decoding uses URLSAFE_NO_PADDING consistently

No data corruption in file transfer

Using same libsodium.js version for both operations

libsodium.js (latest)

Chrome 120+, Firefox 146+

Filesizes: 1MB - 100MB

Dependencies:

LibSodium Version

What could cause crypto_secretstream_xchacha20poly1305_init_pull() to fail even with correct key and header lengths? Are there common pitfalls with the header extraction or key decoding that I might be missing?

Read Entire Article