我正在 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
库敌人加密
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
当前的加密方法使用
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。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()
中使用时会导致发布错误消息。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
的倍数,因此该测试也证明了较小块的正确处理。