/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const KeyedUUIDMapper = Components.Constructor( "@mozilla.org/keyed-uuid-mapper;1", Ci.nsIKeyedUUIDMapper, "init" ); function makeMapper(seed) { // Some key - the specifics do not matter, as long as it is 16 bytes. const key = new Uint8Array(16).map((_, i) => (seed + i) & 0xff); return new KeyedUUIDMapper(key); } // NOTE: All test tasks here are run twice, first in the parent process as // usual, and then again in the content process in the end as a double check, // via test_KeyedUUIDMapper_in_content_process. add_task(function test_uuid_format() { const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; const result = makeMapper(1).toUUID(1); Assert.ok(UUID_RE.test(result), "toUUID() returns a lowercase UUID string"); // The exact value does not matter, we just check a specific value to make // sure that the output is reasonably stable. Assert.equal(result, "c047bf20-c07c-9531-7e69-e6d65bf96acd", "Stable uuid"); // If you ever need to change the above expectation, that means that you are // changing the algorithm. The test_invalid_uuid test tests a boundary case // that you have to re-generate, as follows: // 1. Disable kJS_MAX_SAFE_UINTEGER constraint in KeyedUUIDMapper::ToUUID. // 2. Uncomment the following line: // Assert.ok(false, makeMapper(1).toUUID(Number.MAX_SAFE_INTEGER + 1)); // 3. Run this test, and copy the displayed UUID. // 4. Replace test_invalid_uuid's MAX_SAFE_INTEGER+1 test case with it. }); add_task(function test_roundtrip() { for (const [seed, value] of [ [0, 0], [1, 1], [0xde, 42], [0x12, 0xabcdef], [0x7f, Number.MAX_SAFE_INTEGER], ]) { const mapper = makeMapper(seed); Assert.equal( mapper.fromUUID(mapper.toUUID(value)), value, `seed=${seed}, value=${value}` ); } }); add_task(function test_toUUID_max_range() { const mapper = makeMapper(1); // Beyond MAX_SAFE_INTEGER some numbers may not accurately be represented // in JS. E.g., 2*63 is 9223372036854775808, but ends with 776 000 instead. // The implementation explicitly filters those to avoid loss of precision. Assert.throws( () => mapper.toUUID(Number.MAX_SAFE_INTEGER + 1), // NS_ERROR_INVALID_ARG is converted to NS_ERROR_ILLEGAL_VALUE: /NS_ERROR_ILLEGAL_VALUE/, "toUUID rejects number past MAX_SAFE_INTEGER" ); Assert.throws( () => mapper.toUUID(2 ** 63), // NS_ERROR_INVALID_ARG is converted to NS_ERROR_ILLEGAL_VALUE: /NS_ERROR_ILLEGAL_VALUE/, "toUUID rejects number past MAX_SAFE_INTEGER" ); // Note: values beyond Number.MAX_SAFE_INTEGER are rejected, as shown above. // But beyond the 64-bit numeric range the values become 0 before the // implementation event receives the value. Assert.equal( mapper.fromUUID(mapper.toUUID(2 ** 66)), 0, "toUUID does not support numbers outside the 64-bit range" ); }); add_task(function test_opaque_without_key() { const uuid = makeMapper(0xca).toUUID(0xbabe); // With a different key 0xcb, we should not get value 0xbabe back. // In fact, because 0xbabe is not a value that could ever have been produced // for this given UUID, the implementation throws: Assert.throws( () => makeMapper(0xcb).fromUUID(uuid), /NS_ERROR_ILLEGAL_VALUE/, "wrong key does not recover the original value" ); }); add_task(function test_case_insensitive_parsing() { const lower = makeMapper(1).toUUID(42); const upper = makeMapper(1).toUUID(42).toUpperCase(); Assert.notEqual(lower, upper, "Contains alpha characters"); const mapper = makeMapper(1); Assert.equal(mapper.fromUUID(upper), 42, "fromUUID accepts uppercase hex"); }); add_task(function test_reinit() { const mapper = makeMapper(1); const uuid1 = mapper.toUUID(42); mapper.init(new Uint8Array(16).fill(0xff)); Assert.notEqual( mapper.toUUID(42), uuid1, "reinit with new key changes output" ); mapper.init(new Uint8Array(16).map((_, i) => (1 + i) & 0xff)); Assert.equal( mapper.toUUID(42), uuid1, "reinit with original key restores output" ); }); add_task(function test_not_initialized() { const mapper = Cc["@mozilla.org/keyed-uuid-mapper;1"].createInstance( Ci.nsIKeyedUUIDMapper ); // Note: mapper.init() not called. Assert.throws(() => mapper.toUUID(1), /NS_ERROR_NOT_INITIALIZED/); Assert.throws( () => mapper.fromUUID("00000000-0000-0000-0000-000000000000"), /NS_ERROR_NOT_INITIALIZED/ ); }); add_task(function test_invalid_key_size() { const mapper = Cc["@mozilla.org/keyed-uuid-mapper;1"].createInstance( Ci.nsIKeyedUUIDMapper ); for (const keyLength of [0, 15, 17, 32]) { Assert.throws( () => mapper.init(new Uint8Array(keyLength)), // NS_ERROR_INVALID_ARG is converted to NS_ERROR_ILLEGAL_VALUE: /NS_ERROR_ILLEGAL_VALUE/, `rejects key of length ${keyLength}` ); } }); add_task(function test_invalid_uuid() { const mapper = makeMapper(1); for (const bad of [ "", "not-a-uuid", "00000000-0000-0000-0000-00000000000", // too short "00000000-0000-0000-0000-0000000000000", // too long "00000000-0000-0000-0000-00000000000g", // invalid hex char "000000000000-0000-0000-0000-000000000000", // wrong dash positions // Right length, but no value maps to this. If one inputs a random UUID, // they are in fact more likely going to get an error than a mapping to // an arbitrary value because there are 2^128 UUIDs but only 2^64 valid // values. "00000000-0000-0000-0000-000000000000", // The generated UUID for MAX_SAFE_INTEGER+1 (9007199254740992), // if the implementation would not have capped it at MAX_SAFE_INTEGER. "31bf73c9-08ba-c9b8-1fb7-922e1f311eb4", "{" + mapper.toUUID(1) + "}", // Reject UUID with brackets. ]) { Assert.throws( () => mapper.fromUUID(bad), // NS_ERROR_INVALID_ARG is converted to NS_ERROR_ILLEGAL_VALUE: /NS_ERROR_ILLEGAL_VALUE/, `rejects invalid uuid: "${bad}"` ); } }); add_task(function test_fromUUID_non_ascii() { const mapper = makeMapper(0xde); const uuid = mapper.toUUID(42); // ^ test_roundtrip already verified that fromUUID(uuid) will return 42. // String where the lossy conversion from UTF16 to ASCII would be identical // to the input, but which actually differs from the real input. const bad = String.fromCharCode(uuid.charCodeAt(0) + 0x100) + uuid.slice(1); Assert.throws( () => mapper.fromUUID(bad), // NS_ERROR_INVALID_ARG is converted to NS_ERROR_ILLEGAL_VALUE: /NS_ERROR_ILLEGAL_VALUE/, "Rejects non-ASCII" ); }); // Internally, the mapping is implemented with AES 128. This is merely an // implementation detail. However, we verify that that the behavior matches. // This proof ensures that the mapper inherits these desired properties: // - Confidentiality: Value cannot be recovered from UUID without key. // - Deterministic: For the same sets of inputs, the result is identical. // - Unique: Every input value produces an unique UUID. // - Reversable: A UUID can be mapped back (AES encrypt <-> decrypt). add_task(async function test_verify_alg_AES_128() { // Some arbitrary test values. const keyBytes = new Uint8Array(16).fill(128); const value = 0x123456789; // crypto.subtle does not support AES-ECB, but for the first block it is // equivalent to AES-CBC with a zero IV, so we can use that for verification. const AES_ALGO = "AES-CBC"; const key = await crypto.subtle.importKey( "raw", keyBytes, { name: AES_ALGO }, false, ["encrypt"] ); const plaintext = new Uint8Array(16); new DataView(plaintext.buffer).setBigUint64(0, BigInt(value)); const raw = await crypto.subtle.encrypt( { name: AES_ALGO, iv: new Uint8Array(16) }, key, plaintext ); const uuidBytes = new Uint8Array(raw, 0, 16); // drop padding Assert.equal( new KeyedUUIDMapper(keyBytes).toUUID(value).replaceAll("-", ""), uuidBytes.toHex(), "KeyedUUIDMapper mapping is equivalent to AES-ECB" ); }); if (runningInParent) { // Run same tests again, but in the content process. add_task(async function test_KeyedUUIDMapper_in_content_process() { await run_test_in_child("test_KeyedUUIDMapper.js"); }); }