我正在开发Next.js中的一个服务,该服务使用xchacha20-Poly1305处理文件加密和解密.虽然我已经成功地实现了加密代码,但我在解密代码方面面临着挑战.您能为这个加密功能提供最合适的解密码的指导吗?此外,我还将密码作为来自用户的输入

我正在利用服务工作人员对浏览器中的文件进行加密,以确保它不会影响主线程.

const [file, setFile] = useState();
const [password, setPassword] = useState();
navigator.serviceWorker.ready.then((reg) => {
    if (!reg || !reg.active) {
        setIsEncrypting(false);
        toast.error('Service worker is not ready or its not supported in your browser');
        return;
    }
    reg.active.postMessage({
        cmd: 'encryptFile',
            file,
            password
    });
});

我使用libsodium-wrappers-sumo库加密

service-worker.js

self.addEventListener('install', (event) =>
    event.waitUntil(self.skipWaiting())
);

self.addEventListener('activate', (event) =>
    event.waitUntil(self.clients.claim())
);

const _sodium = require('libsodium-wrappers-sumo');
const STATIC_SIGNATURE = 'Encrypted By XXXXXXX';

(async () => {
    await _sodium.ready;
    const sodium = _sodium;

    addEventListener('message', async (e) => {
        switch (e.data.cmd) {
            case 'encryptFile':
                const startTime = performance.now();
                const { encryptedBlob, encryptedFileName } = await encryptFile(
                    e.data.file,
                    e.data.password
                );

                e.source.postMessage({
                    reply: 'encryptionFinished',
                    encryptedBlob,
                    encryptedFileName,
                });
                break;
        }
    });

    const encryptFile = async (file, password) => {
        // Generate encryption key
        const salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
        const key = sodium.crypto_pwhash(
            sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES,
            sodium.from_string(password),
            salt,
            sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
            sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
            sodium.crypto_pwhash_ALG_ARGON2ID13
        );

        // Initialize encryption
        const { state, header } =
            sodium.crypto_secretstream_xchacha20poly1305_init_push(key);

        // Create a stream controller for chunked processing
        const streamController = new TransformStream();
        const writer = streamController.writable.getWriter();

        // Write signature, salt, and header to the stream
        const signature = sodium.from_string(STATIC_SIGNATURE);
        writer.write(signature);
        writer.write(salt);
        writer.write(header);

        // Encrypt file in chunks
        const chunkSize = 64 * 1024 * 1024;
        const reader = file.stream().getReader();

        while (true) {
            const { done, value } = await reader.read();

            if (done) {
                // Finalize encryption and close the stream
                const encryptedChunk =
                    sodium.crypto_secretstream_xchacha20poly1305_push(
                        state,
                        new Uint8Array(0),
                        null,
                        sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
                    );
                writer.write(encryptedChunk);
                writer.close();

                // Get the encrypted file blob
                const encryptedBlob = await new Response(
                    streamController.readable
                ).blob();

                // send the encrypted file with the original filename + '.enc'
                const encryptedFileName = `${file.name}.enc`;
                return { encryptedBlob, encryptedFileName };
            }

            // Use chunkSize to control the size of each chunk
            for (let i = 0; i < value.length; i += chunkSize) {
                const chunk = value.slice(i, i + chunkSize);
                const encryptedChunk =
                    sodium.crypto_secretstream_xchacha20poly1305_push(
                        state,
                        new Uint8Array(chunk),
                        null,
                        sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE
                    );
                writer.write(encryptedChunk);
            }
        }
    };

})();

现在,我需要一个decryptFile函数来首先判断签名.它应该验证该文件是否由同一平台加密.之后,它应该判断用户提供的密码是否正确,以解密文件.之后,将进行解码过程,逐个块地解密文件块.最后,该函数应该返回decryptedBlob,类似于我在encryptFile函数中实现它的方式,使用const chunkSize = 64 * 1024 * 1024;,并将文件名更改为.enc to non .enc

self.addEventListener('install', (event) =>
    event.waitUntil(self.skipWaiting())
);

self.addEventListener('activate', (event) =>
    event.waitUntil(self.clients.claim())
);

const _sodium = require('libsodium-wrappers-sumo');
const STATIC_SIGNATURE = 'Encrypted By XXXXXXX';

(async () => {
    await _sodium.ready;
    const sodium = _sodium;

    addEventListener('message', async (e) => {
        switch (e.data.cmd) {
            case 'encryptFile':
                ...
                break;
            case 'decryptFile':
                const {decryptedBlob, decryptedFileName} = await decryptFile(e.data.encFile, e.data.password);
                e.source.postMessage({
                    reply: 'decryptionFinished',
                    decryptedBlob,
                    decryptedFileName
                });
                break;
        }
    });

    const encryptFile = async (file, password) => {
        ...
    };

    const decryptFile = async (encFile, password) => {
        ... // help me to write this function
    };

})();

我是网络密码学的新手,目前正在阅读libsodium的文档.然而,我似乎找不到解决办法.请帮我写一下decryptFile函数.

此外,如果您可以建议对当前代码进行任何更改,那将是最受欢迎的.请提供建议以获得更好的性能和可靠性.

const decryptFile = async (file, password) => {
        const signature = await file
            .slice(0, STATIC_SIGNATURE.length)
            .arrayBuffer();
        const decoder = new TextDecoder();

        if (decoder.decode(signature) !== STATIC_SIGNATURE) {
            throw new Error('Invalid signature');
        }

        const saltLength = sodium.crypto_pwhash_SALTBYTES;
        const saltBuffer = await file
            .slice(
                STATIC_SIGNATURE.length,
                STATIC_SIGNATURE.length + saltLength
            )
            .arrayBuffer();
        const salt = new Uint8Array(saltBuffer);

        const header = new Uint8Array(
            await file
                .slice(
                    STATIC_SIGNATURE.length + saltLength,
                    STATIC_SIGNATURE.length +
                        saltLength +
                        sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES
                )
                .arrayBuffer()
        );

        // Generate decryption key
        const key = sodium.crypto_pwhash(
            sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES,
            sodium.from_string(password),
            salt,
            sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
            sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
            sodium.crypto_pwhash_ALG_ARGON2ID13
        );

        const { state_address, tag } =
            sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, key);

        // Create a stream controller for chunked processing
        const streamController = new TransformStream();
        const writer = streamController.writable.getWriter();

        // Decrypt file in chunks
        const chunkSize = 64 * 1024 * 1024;
        const encryptedData = new Uint8Array(
            await file
                .slice(
                    STATIC_SIGNATURE.length +
                        saltLength +
                        sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES
                )
                .arrayBuffer()
        );
        let offset = 0;

        while (offset < encryptedData.length) {
            const chunk = new Uint8Array(
                encryptedData.slice(offset, offset + chunkSize)
            );

            const { message, tag: decryptedTag } =
                sodium.crypto_secretstream_xchacha20poly1305_pull(
                    state_address,
                    new Uint8Array(0),
                    chunk
                );

            if (
                decryptedTag ===
                sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
            ) {
                break; // End of decryption
            }

            writer.write(message);
            offset += chunkSize;
        }

        writer.close();

        // Get the decrypted file blob
        const decryptedBlob = await new Response(
            streamController.readable
        ).blob();
        return decryptedBlob;
    };

这就是我到目前为止创建的内容,但它给出了一个错误,而且也没有首先判断小块上的密码是否正确.

错误如下所示:

service-worker.js:20245 Uncaught (in promise) TypeError: state_address cannot be null or undefined

codesandbox

推荐答案

目前的加密方法读取大小为read()的块,并将它们拆分成大小为chunkSize的较小块.由于read()读取的区块通常不是chunkSize的倍数,因此read()调用的最后一个区块通常小于chunkSize,因此该区块的大小未知,如下所示:

read 1: r, r, r, r, s1, 
read 2: r, r, s2,
read 3: r, r, r, r, r, s3,
...
read n: r, r, r, sn

这里的r是大小为chunkSize的块,而s1,...sn是不同大小的较短的块.

这对应于有效的块序列:

r, r, r, r, s1, r, r, s2, r, r, r, r, r, s3,..., r, r, r, sn

由于密文块的长度未知,intermediate个较小的块不可能正确识别密文块,因此解密失败.

为了避免这种情况,一种方法是暂停处理太短的块,使用read()确定下一个数据,将其附加到太短的块,然后处理结果数据.这可以防止出现太短的块:

read 1: r, r, r, r,  
read 2: r, r, 
read 3: r, r, r, r, r, 
...
read n: r, r, r, sn

r, r, r, r, r, r, r, r, r, r, r,..., r, r, r, sn

因此,只有最后一个块仍然是可能更短的块,这不是问题,因为这个块是由数据的末尾标识的.

要实现这一点,必须更改encryptFile()的代码,例如:

async function encryptFile(file, password){

    // Define chunk size - must be agreed with the decrypting side
    const chunkSize = 192 * 1024;//64 * 1024 * 1024;

    // Generate encryption key
    const salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);                    
    const key = sodium.crypto_pwhash(
        sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES,
        sodium.from_string(password),
        salt,
        sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
        sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
        sodium.crypto_pwhash_ALG_ARGON2ID13
    );
    
    // Initialize encryption
    const { state, header } = sodium.crypto_secretstream_xchacha20poly1305_init_push(key);

    // Create a stream controller for chunked processing
    const streamController = new TransformStream();
    const writer = streamController.writable.getWriter();

    // Write signature, salt, and header to the stream
    const signature = sodium.from_string(STATIC_SIGNATURE);
    writer.write(signature);
    writer.write(salt);
    writer.write(header);                   

    // Encrypt file in chunks
    const reader = file.stream().getReader();
    let dataQueue = new Uint8Array(0) // Queue for data to be encrypted
    while (true) {
        const { done, value } = await reader.read();

        // Add the read data to the queue
        dataQueue = new Uint8Array([...dataQueue, ...new Uint8Array(value)]);
        
        if (done) {
        
            // Finalize encryption and close the stream
            let encryptedChunk = sodium.crypto_secretstream_xchacha20poly1305_push(
                state,
                new Uint8Array(dataQueue),
                null,
                sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
            );
            writer.write(encryptedChunk);
            writer.close();

            // Get the encrypted file blob
            const encryptedBlob = await new Response(
                streamController.readable
            ).blob();

            // send the encrypted file with the original filename + '.enc'
            const encryptedFileName = `${file.name}.enc`;
            return { encryptedBlob, encryptedFileName };
        }

        // Use chunkSize to control the size of each chunk; if the last chunk is smaller than chunkSize 
        // (which is generally the case) it will not be encrypted; the last chunk then remains in the queue;
        // This prevents intermediate chunks that are smaller than chunkSize
        let dataQueueIsEmpty = true;
        for (let i = 0; i < dataQueue.length; i += chunkSize) {
            const chunk = dataQueue.slice(i, i + chunkSize);
            if (chunk.length == chunkSize) {
                const encryptedChunk = sodium.crypto_secretstream_xchacha20poly1305_push(
                    state,
                    new Uint8Array(chunk),
                    null,
                    sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE
                );
                writer.write(encryptedChunk);
            }
            else {
                dataQueue = chunk;
                dataQueueIsEmpty = false;
            }
        }
        if (dataQueueIsEmpty) {dataQueue = new Uint8Array(0)}
    }
}

解密必须适应更改后的加密.还必须考虑到:

  • A ciphertext chunk is larger than a plaintext chunk. The constant sodium.crypto_secretstream_xchacha20poly1305_ABYTES specifies the number of additional bytes. This must be considered during decryption when defining the chunk size: chunkSize (dec) = chunkSize(enc) + sodium.crypto_secretstream_xchacha20poly1305_ABYTES, see this example in the Libsodium documentation.
    This is not taken into account in the current code, which is another reason why the decryption fails.
  • libsodium-wrapper-sumo文档中的这example说明了如何使用sodium.crypto_secretstream_xchacha20poly1305_init_pull()sodium.crypto_secretstream_xchacha20poly1305_pull().这两种方法在当前代码中都未正确使用.这会导致sodium.crypto_secretstream_xchacha20poly1305_init_pull()state_address返回undefined,稍后在sodium.crypto_secretstream_xchacha20poly1305_pull()中使用时会导致发布的错误消息.
  • 加密后的数据在开头包含签名、SALT和头部.在接下来的实现中,首先进行read()次调用(或在必要时进行多次调用),直到确定该数据为止.然后处理before剩余数据,使用read()来确定进一步的数据(这不是绝对必要的,但这是该实现方式以避免过大的数据量).

以下代码是一种可能的实现:

async function decryptFile(file, password) {

    // Define chunk size - must be agreed with the enrypting side
    const chunkSize = 192 * 1024 + sodium.crypto_secretstream_xchacha20poly1305_ABYTES; //64 * 1024 * 1024;
                        
    // Get signature, salt and header
    const reader = file.stream().getReader();
    let dataQueue = new Uint8Array(0); // Queue for data to be encrypted
    while (dataQueue.byteLength < STATIC_SIGNATURE.length + sodium.crypto_pwhash_SALTBYTES + sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES) {
        const { done, value } = await reader.read();

        // Add the read data to the queue
        dataQueue = new Uint8Array([...dataQueue, ...new Uint8Array(value)]);
    }
    const signature = dataQueue.slice(0, STATIC_SIGNATURE.length);
    const salt = dataQueue.slice(STATIC_SIGNATURE.length, STATIC_SIGNATURE.length + sodium.crypto_pwhash_SALTBYTES);
    const header = dataQueue.slice(STATIC_SIGNATURE.length + sodium.crypto_pwhash_SALTBYTES, STATIC_SIGNATURE.length + sodium.crypto_pwhash_SALTBYTES + sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES);
    dataQueue = dataQueue.slice(STATIC_SIGNATURE.length + sodium.crypto_pwhash_SALTBYTES + sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES);
    
    // Generate decryption key
    const key = sodium.crypto_pwhash(
        sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES,
        sodium.from_string(password),
        salt,
        sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
        sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
        sodium.crypto_pwhash_ALG_ARGON2ID13
    );
    
    // Initialize decryption
    let state = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, key);

    // Create a stream controller for chunked processing
    const streamController = new TransformStream();
    const writer = streamController.writable.getWriter();

    // Loop for large chunks that were fetched with read() (containing multiple small chunks with size chunkSize)
    let dataWithHeader = true;
    while (true) {
    
        let done; 
        if (!dataWithHeader){
            // Add read data to queue
            let data = await reader.read();
            done = data.done;
            dataQueue = new Uint8Array([...dataQueue, ...new Uint8Array(data.value)]);
        } else {
            // Skip adding as queue still filled
            dataWithHeader = false;
            done = false;
        }
        
        if (done) {                     
            // Finalize decryption and close the stream
            let decryptedChunk = sodium.crypto_secretstream_xchacha20poly1305_pull(
                state,
                new Uint8Array(dataQueue)
            );
            // optional check: decryptedChunk.tag must be sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
            writer.write(decryptedChunk.message);
            writer.close();
            // Get the decrypted file blob
            const decryptedBlob = await new Response(
                streamController.readable
            ).blob();
            // Send the decrypted file with the original filename + '.enc'
            const decryptedFileName = `${file.name}.enc`;
            return { decryptedBlob, decryptedFileName };
        }

        // Loop for small chunks with size chunkSize: Split the large chunks in chunks of size chunkSize.  
        // If the last chunk is smaller than chunkSize (which is generally the case) it will not be decrypted
        // and remains in the queue. This prevents intermediate chunks that are smaller than chunkSize.
        let dataQueueIsEmpty = true;
        for (let i = 0; i < dataQueue.length; i += chunkSize) {
            const chunk = dataQueue.slice(i, i + chunkSize);
            if (chunk.length == chunkSize) {
                const decryptedChunk = sodium.crypto_secretstream_xchacha20poly1305_pull(
                    state,
                    new Uint8Array(chunk),
                );
                // optional check: decryptedChunk.tag must be sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE
                writer.write(decryptedChunk.message);
            }
            else {
                dataQueue = chunk;
                dataQueueIsEmpty = false;
            }
        }
        if (dataQueueIsEmpty) {dataQueue = new Uint8Array(0);}
    }
};

我已经成功地对一个14072985字节的文件和192*1024字节的块大小的文件进行了加密和解密测试.使用这些参数,将执行几个读取数据不对应于chunkSize的倍数的读取调用,因此该测试还证明了对较小区块的正确处理.

enter image description here


Edit:

Since the primary goal was to have a working logic and to fix the bugs, the code does not contain any error handling and is not performance-optimized!
I.e. the exception handling must therefore be added according to your requirements as well as a performance optimization.


Regarding the password: crypto_secretstream_xchacha20poly1305_pull() returns message and tag in case of success and false in case of an error (e.g. invalid/incomplete/corrupted ciphertext, wrong password etc.).
In your code, there are generally several crypto_secretstream_xchacha20poly1305_pull() calls. If one of these calls returns false, the data has been tampered with, intentionally or unintentionally, and the decryption must be considered failed (and all data should be discarded as a precaution).

在密码错误的情况下,第一次调用已经返回false,因此可以快速识别错误.


Regarding the performance issue: The main reason for the performance problem is dataQueue and the copy operations used to fill it, i.e. the line: dataQueue = new Uint8Array([...dataQueue, ...new Uint8Array(value)]).
These copy operations become increasingly inperformant as the data size increases. This can be easily verified by outputting the execution time for this line together with dataQueue.length:

enter image description here

This problem is basically related to the use of a fixed chunk size. The fixed chunk size requires (because of the shorter chunks) a reorganization of the chunks into chunks of the given chunk size, which is associated with copying operations, appending etc.
I have solved this problem in my sample implementation mainly for the sake of simplicity with dataQueue.

可以做些什么来提高性能?首先,existing代码可以进行性能优化.例如,在填充dataQueue之前,可以判断它是否为空.如果是这样的话,dataQueue = value就足够了,而不是昂贵的复制过程.可能会有这种类型的进一步优化 Select .

Such optimizations should require the least effort, but may not be sufficient. It would then be necessary to consider how the small chunks and the new data read in with read() could be converted into chunks of the given chunk size as efficiently as possible, i.e. with as few copy operations and/or as little copied data as possible, so that dataQueue can be dispensed with.
For this, e.g. the first chunk after a new read() call could be filled with the data of the last, too short chunk (which must be saved for this purpose somewhere) and the rest with the data of the new read() call. Subsequent chunks are then only filled with the data from the new read() call. The filling would therefore be more direct here and would not run via dataQueue. Although copying processes are also necessary here, the amount of data should be smaller.
A further optimization would of course be if copying/appending itself were more performant. Possibly, e.g. the use of regular arrays instead of typed arrays or something similar would provide a performance gain.
All in all, however, the optimizations described in this section require a larger amount of code changes and trial and error.

另一种 Select ,但是完全不同的方法,将是省go 固定的块大小并且在密文中存储未加密的块大小信息(类似于签名、盐和报头),例如通过在相关块之前的前4个字节中存储块大小.这将使在解密期间识别块成为可能.没有必要重新组织这些数据块.由于在解密过程中会判断块的顺序,因此不存在与此方法相关的漏洞.然而,该解决方案实际上相当于一种新的实现,因此涉及最大的更改工作.

Reactjs相关问答推荐

有没有方法覆盖NextJS 14中的布局?

可选链和useState挂钩

下拉Tailwind CSS菜单与怪异beviour

蚂蚁 Select .列表弹出窗口不会';有时不会出现

在reactjs中刷新页面时丢失状态值

验证 Yup 中的对象项

将 useRef 与 useImperativeHandle 一起使用时,React 不会重新渲染

从 React 应用程序调用未经身份验证的 graphql 查询时出现错误:没有当前用户

将水平滚动视图添加到底部导航选项卡

使用登录保护 React 中的 SPA 应用程序 - 为什么这种方法不起作用?

下一个js如何从另一个组件更新组件?

Primereact 的日历组件在每个月 12 日之后使用非唯一键生成

Next.js i18n 路由不适用于动态路由

React Three Fiber mesh 标签在此浏览器中无法识别

使用 axios 响应路由 Dom 操作

null 不是对象(判断RNSound.IsAndroid)

如何使用 cypress 获取 MUI TextField 的输入值?

当页面重新加载react 路由导航到第一页 v6

在 redux 状态更改时更新react 按钮禁用状态

Map 函数只返回 Array 中的图像