javascriptreactjsencryption-symmetricopenpgpopenpgp.js

Streaming decrypt using openpgpjs library consumes too much memory for large files


I am using openpgp library to encrypt and decrypt a file inside a React component. Encryption works as expected with less memory usage but decryption takes too much memory. Library version that I am using is: 5.7.0

Below is the code sample with encryption and decryption function. Encryption type is symmetric with password.

import React, { useState } from "react";
import * as openpgp from "openpgp";

const FileEncrypter = () => {
    const [file, setFile] = useState(null);
    const [password, setPassword] = useState("");
    const [encryptedFile, setEncryptedFile] = useState(null);
    const [decryptedFile, setDecryptedFile] = useState(null);

    const handleFileChange = (e) => {
        const selectedFile = e.target.files[0];
        setFile(selectedFile);
    };

    const handlePasswordChange = (e) => {
        setPassword(e.target.value);
    };

    const fileToStream = (file) => {
        const blob = new Blob([file], { type: file.type });
        const stream = blob.stream();
        return stream;
    }


    const streamEncryptFile = async (fileToEncrypt) => {

        const fileName = `${file.name}.pgp`;

        const readableStream = fileToStream(fileToEncrypt);
        const message = await openpgp.createMessage({ binary: readableStream });
        const encrypted = await openpgp.encrypt({
            message, // input as Message object
            passwords: [password], // multiple passwords possible
            format: 'binary', // don't ASCII armor (for Uint8Array output)
            config: { preferredCompressionAlgorithm: openpgp.enums.compression.zlib } // compress the data with zlib
        });

       
        const blob = await webStreamToBlob(encrypted);
        console.log("Encrypt Output Size: ", formatBytes(blob.size));

        return new File([blob], fileName, { type: 'application/octet-stream' });

    }

    const streamDecryptFile = async (fileToDecrypt) => {

        const fileName = `${file.name}`;

        const readableStream = fileToStream(fileToDecrypt);
        const message = await openpgp.readMessage({ binaryMessage: readableStream });
        const decrypted = await openpgp.decrypt({
            message, // input as Message object
            passwords: [password], // multiple passwords possible
            format: 'binary', // don't ASCII armor (for Uint8Array output)
            config: { preferredCompressionAlgorithm: openpgp.enums.compression.zlib } // compress the data with zlib

        });

        const blob = await webStreamToBlob(decrypted.data);
        console.log("Decrypt Output Size: ", formatBytes(blob.size));

        return new File([blob], fileName, { type: 'application/octet-stream' });

    }

    const webStreamToBlob = async (webStream) => {
        try {
            const reader = webStream.getReader();
            //const reader = webStream.getReader({ chunkSize: 1 * 1024 * 1024 });
            const chunks = [];
            let done, value;
            while (!done) {
                ({ done, value } = await reader.read());
                if (value) {
                    console.log("Chunk Count: ", chunks.length + 1);
                    chunks.push(value);
                }
            }
            const blob = new Blob(chunks, { type: 'application/octet-stream' });
            return blob;
        } catch (error) {
            console.error('Error in coverting to blob:', error);
            //throw new Error('Failed to convert WebStream to Blob.');
        }
    }

    const formatBytes = (bytes, decimals = 2) => {
        if (!+bytes) return '0 Bytes'

        const k = 1024
        const dm = decimals < 0 ? 0 : decimals
        const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']

        const i = Math.floor(Math.log(bytes) / Math.log(k))

        return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
    }

    const handleEncryptClick = async () => {

        const encryptedFile = await streamEncryptFile(file);
        setEncryptedFile(encryptedFile);

    };

    const handleDecryptClick = async () => {

        const decryptedFile = await streamDecryptFile(encryptedFile);
        setDecryptedFile(decryptedFile);

    };

    return (
        <div>
            <input type="file" onChange={handleFileChange} />
            <input type="text" placeholder="Password" onChange={handlePasswordChange} />
            <button onClick={handleEncryptClick}>Encrypt</button>
            {encryptedFile && <button onClick={handleDecryptClick}>Decrypt</button>}
            <br />
            {encryptedFile && (
                <a href={URL.createObjectURL(encryptedFile)} download={`${file.name}.pgp`}>
                    Download encrypted file
                </a>
            )}
            <br />
            {decryptedFile && (
                <a href={URL.createObjectURL(decryptedFile)} download={`${file.name}`}>
                    Download decrypted file
                </a>
            )}
        </div>
    );
};

export default FileEncrypter;


Solution

  • Thanks to the discussion over at GitHub I am now able to stream decrypt. The issue was due to missing allowUnauthenticatedStream: true config option. When set to false OpenPGP library tries to buffer the complete file and hence using more memory. The correct way to decrypt is to pipe the stream to a WritableStream and save the file to disk. You can refer to updated code here at GitHub