XChaCha20-Poly1305的解密功能

问题描述 投票:0回答:1

我正在 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

代码沙盒

reactjs encryption cryptography libsodium
1个回答
0
投票

当前的加密方法使用

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

中间较小的块由于其长度未知而无法正确识别密文块,从而导致解密失败。

为了避免这种情况,一种方法是暂停处理太短的块,使用

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)}
    }
}

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

  • 密文块比明文块大。常量
    sodium.crypto_secretstream_xchacha20poly1305_ABYTES
    指定附加字节数。在解密过程中定义块大小时必须考虑这一点:
    chunkSize (dec) = chunkSize(enc) + sodium.crypto_secretstream_xchacha20poly1305_ABYTES
    ,请参阅 Libsodium 文档中的example
    当前代码中没有考虑到这一点,这也是解密失败的另一个原因。
  • 来自 libsodium-wrapper-sumo 文档的示例说明了如何使用
    sodium.crypto_secretstream_xchacha20poly1305_init_pull()
    sodium.crypto_secretstream_xchacha20poly1305_pull()
    。两者在当前代码中都被错误地使用。这会导致
    sodium.crypto_secretstream_xchacha20poly1305_init_pull()
    返回
    undefined
    state_address
    ,稍后在
    sodium.crypto_secretstream_xchacha20poly1305_pull()
    中使用时会导致发布错误消息。
  • 加密数据开头包含签名、salt 和 header。在下面的实现中,首先进行
    read()
    调用(或者必要时进行多次),直到确定该数据。然后,在使用 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的倍数,因此该测试也证明了较小块的正确处理。

    

© www.soinside.com 2019 - 2024. All rights reserved.