322 lines
8.9 KiB
JavaScript
322 lines
8.9 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
https://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
"use strict";
|
|
|
|
const { ArchiveEncryptionState } = ChromeUtils.importESModule(
|
|
"resource:///modules/backup/ArchiveEncryptionState.sys.mjs"
|
|
);
|
|
const { ArchiveEncryptor, ArchiveDecryptor } = ChromeUtils.importESModule(
|
|
"resource:///modules/backup/ArchiveEncryption.sys.mjs"
|
|
);
|
|
const { ArchiveUtils } = ChromeUtils.importESModule(
|
|
"resource:///modules/backup/ArchiveUtils.sys.mjs"
|
|
);
|
|
|
|
const TEST_RECOVERY_CODE = "This is my recovery code.";
|
|
|
|
const FAKE_BYTES_AMOUNT = 1000;
|
|
|
|
let fakeBytes = null;
|
|
|
|
add_setup(async () => {
|
|
fakeBytes = new Uint8Array(FAKE_BYTES_AMOUNT);
|
|
// seededRandomNumberGenerator is defined in head.js, but eslint doesn't seem
|
|
// happy about it. Maybe that's because it's a generator function.
|
|
// eslint-disable-next-line no-undef
|
|
let gen = seededRandomNumberGenerator();
|
|
for (let i = 0; i < FAKE_BYTES_AMOUNT; ++i) {
|
|
fakeBytes.set(gen.next().value, i);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Tests that we can construct an ArchiveEncryptor by way of the properties
|
|
* of an ArchiveEncryptionState.
|
|
*/
|
|
add_task(async function test_ArchiveEncryptor_initializer() {
|
|
let { instance: encState } =
|
|
await ArchiveEncryptionState.initialize(TEST_RECOVERY_CODE);
|
|
let encryptor = await ArchiveEncryptor.initialize(
|
|
encState.publicKey,
|
|
encState.backupAuthKey
|
|
);
|
|
Assert.ok(encryptor, "An ArchiveEncryptor was successfully constructed");
|
|
});
|
|
|
|
/**
|
|
* Tests that we can encrypt a single chunk of bytes.
|
|
*/
|
|
add_task(async function test_ArchiveEncryption_single_chunk() {
|
|
let { instance: encState } =
|
|
await ArchiveEncryptionState.initialize(TEST_RECOVERY_CODE);
|
|
let encryptor = await ArchiveEncryptor.initialize(
|
|
encState.publicKey,
|
|
encState.backupAuthKey
|
|
);
|
|
|
|
const TEST_METADATA = { test: "hello!" };
|
|
let jsonBlock = await encryptor.confirm(
|
|
TEST_METADATA,
|
|
encState.wrappedSecrets,
|
|
encState.salt,
|
|
encState.nonce
|
|
);
|
|
// Ensure that the JSON block can be serialized to string, and deserialized
|
|
// again.
|
|
jsonBlock = JSON.parse(JSON.stringify(jsonBlock));
|
|
|
|
let encryptedBytes = await encryptor.encrypt(
|
|
fakeBytes,
|
|
true /* isLastChunk */
|
|
);
|
|
|
|
// Ensure the the encrypted bytes do not match the plaintext bytes.
|
|
Assert.greater(
|
|
encryptedBytes.byteLength,
|
|
fakeBytes.byteLength,
|
|
"Encrypted bytes should be larger"
|
|
);
|
|
|
|
assertUint8ArraysSimilarity(
|
|
encryptedBytes,
|
|
fakeBytes,
|
|
false /* expectSimilar */
|
|
);
|
|
|
|
let decryptor = await ArchiveDecryptor.initialize(
|
|
TEST_RECOVERY_CODE,
|
|
jsonBlock
|
|
);
|
|
Assert.ok(decryptor, "Got back an initialized ArchiveDecryptor");
|
|
|
|
let decryptedBytes = await decryptor.decrypt(encryptedBytes, true);
|
|
|
|
Assert.equal(
|
|
decryptedBytes.byteLength,
|
|
fakeBytes.byteLength,
|
|
"Decrypted bytes should have original length"
|
|
);
|
|
|
|
assertUint8ArraysSimilarity(
|
|
decryptedBytes,
|
|
fakeBytes,
|
|
true /* expectSimilar */
|
|
);
|
|
});
|
|
|
|
/**
|
|
* Tests that we can encrypt an unevenly sized set of chunks.
|
|
*/
|
|
add_task(async function test_ArchiveEncryption_uneven_chunks() {
|
|
let { instance: encState } =
|
|
await ArchiveEncryptionState.initialize(TEST_RECOVERY_CODE);
|
|
let encryptor = await ArchiveEncryptor.initialize(
|
|
encState.publicKey,
|
|
encState.backupAuthKey
|
|
);
|
|
|
|
const TEST_METADATA = { test: "hello!" };
|
|
let jsonBlock = await encryptor.confirm(
|
|
TEST_METADATA,
|
|
encState.wrappedSecrets,
|
|
encState.salt,
|
|
encState.nonce
|
|
);
|
|
// Ensure that the JSON block can be serialized to string, and deserialized
|
|
// again.
|
|
jsonBlock = JSON.parse(JSON.stringify(jsonBlock));
|
|
|
|
// FAKE_BYTES_AMOUNT / 3 shouldn't divide cleanly. So our chunks will have the
|
|
// following byte indices:
|
|
//
|
|
// - 0, 332 (333 bytes)
|
|
// - 333, 666 (333 bytes)
|
|
// - 667, 999 (332 bytes)
|
|
//
|
|
// Note that subarray's "end" argument is _exclusive_.
|
|
let sandbox = sinon.createSandbox();
|
|
sandbox.stub(ArchiveUtils, "ARCHIVE_CHUNK_MAX_BYTES_SIZE").get(() => {
|
|
return 333;
|
|
});
|
|
|
|
let firstChunk = fakeBytes.subarray(0, 333);
|
|
Assert.equal(firstChunk.byteLength, 333);
|
|
let secondChunk = fakeBytes.subarray(333, 666);
|
|
Assert.equal(secondChunk.byteLength, 333);
|
|
let thirdChunk = fakeBytes.subarray(667, 999);
|
|
Assert.equal(thirdChunk.byteLength, 332);
|
|
|
|
let encryptedFirstChunk = await encryptor.encrypt(firstChunk);
|
|
let encryptedSecondChunk = await encryptor.encrypt(secondChunk);
|
|
let encryptedThirdChunk = await encryptor.encrypt(
|
|
thirdChunk,
|
|
true /*isLastChunk */
|
|
);
|
|
|
|
let encryptedPairsToCompare = [
|
|
[firstChunk, encryptedFirstChunk],
|
|
[secondChunk, encryptedSecondChunk],
|
|
[thirdChunk, encryptedThirdChunk],
|
|
];
|
|
|
|
for (let [chunk, encryptedChunk] of encryptedPairsToCompare) {
|
|
assertUint8ArraysSimilarity(
|
|
chunk,
|
|
encryptedChunk,
|
|
false /* expectSimilar */
|
|
);
|
|
}
|
|
|
|
let decryptor = await ArchiveDecryptor.initialize(
|
|
TEST_RECOVERY_CODE,
|
|
jsonBlock
|
|
);
|
|
Assert.ok(decryptor, "Got back an initialized ArchiveDecryptor");
|
|
|
|
let decryptedFirstChunk = await decryptor.decrypt(encryptedFirstChunk);
|
|
let decryptedSecondChunk = await decryptor.decrypt(encryptedSecondChunk);
|
|
let decryptedThirdChunk = await decryptor.decrypt(
|
|
encryptedThirdChunk,
|
|
true /* isLastChunk */
|
|
);
|
|
|
|
let decryptedPairsToCompare = [
|
|
[firstChunk, decryptedFirstChunk],
|
|
[secondChunk, decryptedSecondChunk],
|
|
[thirdChunk, decryptedThirdChunk],
|
|
];
|
|
|
|
for (let [chunk, decryptedChunk] of decryptedPairsToCompare) {
|
|
Assert.equal(
|
|
chunk.byteLength,
|
|
decryptedChunk.byteLength,
|
|
"Decrypted bytes should have original length"
|
|
);
|
|
assertUint8ArraysSimilarity(
|
|
chunk,
|
|
decryptedChunk,
|
|
true /* expectSimilar */
|
|
);
|
|
}
|
|
sandbox.restore();
|
|
});
|
|
|
|
/**
|
|
* Tests that we can encrypt an even sized set of chunks.
|
|
*/
|
|
add_task(async function test_ArchiveEncryption_even_chunks() {
|
|
let { instance: encState } =
|
|
await ArchiveEncryptionState.initialize(TEST_RECOVERY_CODE);
|
|
let encryptor = await ArchiveEncryptor.initialize(
|
|
encState.publicKey,
|
|
encState.backupAuthKey
|
|
);
|
|
|
|
const TEST_METADATA = { test: "hello!" };
|
|
let jsonBlock = await encryptor.confirm(
|
|
TEST_METADATA,
|
|
encState.wrappedSecrets,
|
|
encState.salt,
|
|
encState.nonce
|
|
);
|
|
// Ensure that the JSON block can be serialized to string, and deserialized
|
|
// again.
|
|
jsonBlock = JSON.parse(JSON.stringify(jsonBlock));
|
|
|
|
// FAKE_BYTES_AMOUNT / 2 should divide evenly. So our chunks will have the
|
|
// following byte indices:
|
|
//
|
|
// - 0, 499 (500 bytes)
|
|
// - 500, 999 (500 bytes)
|
|
//
|
|
// Note that subarray's "end" argument is _exclusive_.
|
|
let sandbox = sinon.createSandbox();
|
|
sandbox.stub(ArchiveUtils, "ARCHIVE_CHUNK_MAX_BYTES_SIZE").get(() => {
|
|
return 500;
|
|
});
|
|
|
|
let firstChunk = fakeBytes.subarray(0, 500);
|
|
Assert.equal(firstChunk.byteLength, 500);
|
|
let secondChunk = fakeBytes.subarray(500);
|
|
Assert.equal(secondChunk.byteLength, 500);
|
|
|
|
let encryptedFirstChunk = await encryptor.encrypt(firstChunk);
|
|
let encryptedSecondChunk = await encryptor.encrypt(
|
|
secondChunk,
|
|
true /*isLastChunk */
|
|
);
|
|
|
|
let encryptedPairsToCompare = [
|
|
[firstChunk, encryptedFirstChunk],
|
|
[secondChunk, encryptedSecondChunk],
|
|
];
|
|
|
|
for (let [chunk, encryptedChunk] of encryptedPairsToCompare) {
|
|
assertUint8ArraysSimilarity(
|
|
chunk,
|
|
encryptedChunk,
|
|
false /* expectSimilar */
|
|
);
|
|
}
|
|
|
|
let decryptor = await ArchiveDecryptor.initialize(
|
|
TEST_RECOVERY_CODE,
|
|
jsonBlock
|
|
);
|
|
Assert.ok(decryptor, "Got back an initialized ArchiveDecryptor");
|
|
|
|
let decryptedFirstChunk = await decryptor.decrypt(encryptedFirstChunk);
|
|
let decryptedSecondChunk = await decryptor.decrypt(
|
|
encryptedSecondChunk,
|
|
true /* isLastChunk */
|
|
);
|
|
|
|
let decryptedPairsToCompare = [
|
|
[firstChunk, decryptedFirstChunk],
|
|
[secondChunk, decryptedSecondChunk],
|
|
];
|
|
|
|
for (let [chunk, decryptedChunk] of decryptedPairsToCompare) {
|
|
Assert.equal(
|
|
chunk.byteLength,
|
|
decryptedChunk.byteLength,
|
|
"Decrypted bytes should have original length"
|
|
);
|
|
assertUint8ArraysSimilarity(
|
|
chunk,
|
|
decryptedChunk,
|
|
true /* expectSimilar */
|
|
);
|
|
}
|
|
sandbox.restore();
|
|
});
|
|
|
|
/**
|
|
* Tests that we cannot decrypt with the wrong recovery code.
|
|
*/
|
|
add_task(async function test_ArchiveEncryption_wrong_recoveryCode() {
|
|
let { instance: encState } =
|
|
await ArchiveEncryptionState.initialize(TEST_RECOVERY_CODE);
|
|
let encryptor = await ArchiveEncryptor.initialize(
|
|
encState.publicKey,
|
|
encState.backupAuthKey
|
|
);
|
|
|
|
const TEST_METADATA = { test: "hello!" };
|
|
let jsonBlock = await encryptor.confirm(
|
|
TEST_METADATA,
|
|
encState.wrappedSecrets,
|
|
encState.salt,
|
|
encState.nonce
|
|
);
|
|
|
|
// We don't actually care about the encrypted bytes, since we're just
|
|
// testing that ArchiveDecryptor won't accept an incorrect recovery code.
|
|
await encryptor.encrypt(fakeBytes, true /* isLastChunk */);
|
|
|
|
await Assert.rejects(
|
|
ArchiveDecryptor.initialize("Wrong recovery code", jsonBlock),
|
|
/Unauthenticated/
|
|
);
|
|
});
|