mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-11-04 00:00:28 +09:00
104 lines
3.4 KiB
TypeScript
104 lines
3.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { encryptChunk, generateDataKey } from "#lib/shared/crypto/index.js";
|
|
import {
|
|
CHUNK_SIZE,
|
|
createDecryptionStream,
|
|
getEncryptedRange,
|
|
getEncryptedSize,
|
|
} from "#lib/shared/format/index.js";
|
|
import { concatenateBuffers } from "#lib/shared/crypto/index.js";
|
|
|
|
const plaintext = Uint8Array.from({ length: 2 * CHUNK_SIZE + 12345 }, (_, i) => (i * 7) % 251);
|
|
|
|
// `toEqual` is too slow for large arrays
|
|
const expectBytes = (actual: Uint8Array, expected: Uint8Array) => {
|
|
expect(actual.byteLength).toBe(expected.byteLength);
|
|
expect(Buffer.from(actual).equals(Buffer.from(expected))).toBe(true);
|
|
};
|
|
|
|
const encrypt = async (data: Uint8Array, dataKey: CryptoKey, chunkSize = CHUNK_SIZE) => {
|
|
const chunks: ArrayBuffer[] = [];
|
|
for (let offset = 0; offset < data.byteLength; offset += chunkSize) {
|
|
chunks.push(await encryptChunk(data.slice(offset, offset + chunkSize).buffer, dataKey));
|
|
}
|
|
return concatenateBuffers(...chunks);
|
|
};
|
|
|
|
// Feeds the input in pieces of irregular sizes like a network stream
|
|
const toStream = (data: Uint8Array) =>
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
let offset = 0;
|
|
for (let i = 0; offset < data.byteLength; i++) {
|
|
const size = [1, 1000, 65536, 3_000_000][i % 4]!;
|
|
controller.enqueue(data.slice(offset, offset + size));
|
|
offset += size;
|
|
}
|
|
controller.close();
|
|
},
|
|
});
|
|
|
|
const decrypt = async (
|
|
encrypted: Uint8Array,
|
|
dataKey: CryptoKey,
|
|
start: number,
|
|
end: number,
|
|
isLegacy = false,
|
|
) => {
|
|
const encryptedRange = getEncryptedRange(start, end, encrypted.byteLength, isLegacy);
|
|
const stream = toStream(
|
|
encrypted.slice(encryptedRange.start, encryptedRange.end + 1),
|
|
).pipeThrough(
|
|
createDecryptionStream({
|
|
range: { start, end, total: plaintext.byteLength },
|
|
chunkCount: encryptedRange.lastChunkIndex - encryptedRange.firstChunkIndex + 1,
|
|
dataKey,
|
|
isLegacy,
|
|
}),
|
|
);
|
|
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
};
|
|
|
|
describe("createDecryptionStream", async () => {
|
|
const { dataKey } = await generateDataKey();
|
|
const encrypted = await encrypt(plaintext, dataKey);
|
|
|
|
it("produces encrypted content of the expected size", () => {
|
|
expect(encrypted.byteLength).toBe(getEncryptedSize(plaintext.byteLength));
|
|
});
|
|
|
|
it.each([
|
|
[0, plaintext.byteLength - 1],
|
|
[0, 0],
|
|
[10, 20],
|
|
[CHUNK_SIZE - 5, CHUNK_SIZE + 5],
|
|
[CHUNK_SIZE, 2 * CHUNK_SIZE - 1],
|
|
[CHUNK_SIZE + 1, plaintext.byteLength - 1],
|
|
[plaintext.byteLength - 1, plaintext.byteLength - 1],
|
|
])("decrypts the range %i-%i", async (start, end) => {
|
|
expectBytes(await decrypt(encrypted, dataKey, start, end), plaintext.slice(start, end + 1));
|
|
});
|
|
|
|
it("decrypts ranges of legacy files", async () => {
|
|
const legacyEncrypted = await encrypt(plaintext, dataKey, plaintext.byteLength);
|
|
|
|
expectBytes(
|
|
await decrypt(legacyEncrypted, dataKey, 100, CHUNK_SIZE + 100, true),
|
|
plaintext.slice(100, CHUNK_SIZE + 101),
|
|
);
|
|
});
|
|
|
|
it("rejects truncated content", async () => {
|
|
const stream = toStream(encrypted.slice(0, CHUNK_SIZE)).pipeThrough(
|
|
createDecryptionStream({
|
|
range: { start: 0, end: plaintext.byteLength - 1, total: plaintext.byteLength },
|
|
chunkCount: 3,
|
|
dataKey,
|
|
isLegacy: false,
|
|
}),
|
|
);
|
|
|
|
await expect(new Response(stream).arrayBuffer()).rejects.toThrow();
|
|
});
|
|
});
|