tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

encap_decap_keys.tentative.https.any.js (13789B)


      1 // META: title=WebCryptoAPI: ML-KEM encapsulateKey() and decapsulateKey() tests
      2 // META: script=ml_kem_vectors.js
      3 // META: script=../util/helpers.js
      4 // META: timeout=long
      5 
      6 function define_key_tests() {
      7  var subtle = self.crypto.subtle;
      8  var variants = ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024'];
      9 
     10  // Test various 256-bit shared key algorithms
     11  var sharedKeyConfigs = [
     12    {
     13      algorithm: { name: 'AES-GCM', length: 256 },
     14      usages: ['encrypt', 'decrypt'],
     15      description: 'AES-GCM-256',
     16    },
     17    {
     18      algorithm: { name: 'AES-CBC', length: 256 },
     19      usages: ['encrypt', 'decrypt'],
     20      description: 'AES-CBC-256',
     21    },
     22    {
     23      algorithm: { name: 'AES-CTR', length: 256 },
     24      usages: ['encrypt', 'decrypt'],
     25      description: 'AES-CTR-256',
     26    },
     27    {
     28      algorithm: { name: 'AES-KW', length: 256 },
     29      usages: ['wrapKey', 'unwrapKey'],
     30      description: 'AES-KW-256',
     31    },
     32    {
     33      algorithm: { name: 'HMAC', hash: 'SHA-256' },
     34      usages: ['sign', 'verify'],
     35      description: 'HMAC-SHA-256',
     36    },
     37  ];
     38 
     39  variants.forEach(function (algorithmName) {
     40    sharedKeyConfigs.forEach(function (config) {
     41      // Test encapsulateKey operation
     42      promise_test(async function (test) {
     43        // Generate a key pair for testing
     44        var keyPair = await subtle.generateKey({ name: algorithmName }, false, [
     45          'encapsulateKey',
     46          'decapsulateKey',
     47        ]);
     48 
     49        // Test encapsulateKey
     50        var encapsulatedKey = await subtle.encapsulateKey(
     51          { name: algorithmName },
     52          keyPair.publicKey,
     53          config.algorithm,
     54          true,
     55          config.usages
     56        );
     57 
     58        assert_true(
     59          encapsulatedKey instanceof Object,
     60          'encapsulateKey should return an object'
     61        );
     62        assert_true(
     63          encapsulatedKey.hasOwnProperty('sharedKey'),
     64          'Result should have sharedKey property'
     65        );
     66        assert_true(
     67          encapsulatedKey.hasOwnProperty('ciphertext'),
     68          'Result should have ciphertext property'
     69        );
     70        assert_true(
     71          encapsulatedKey.sharedKey instanceof CryptoKey,
     72          'sharedKey should be a CryptoKey'
     73        );
     74        assert_true(
     75          encapsulatedKey.ciphertext instanceof ArrayBuffer,
     76          'ciphertext should be ArrayBuffer'
     77        );
     78 
     79        // Verify the shared key properties
     80        assert_equals(
     81          encapsulatedKey.sharedKey.type,
     82          'secret',
     83          'Shared key should be secret type'
     84        );
     85        assert_equals(
     86          encapsulatedKey.sharedKey.algorithm.name,
     87          config.algorithm.name,
     88          'Shared key algorithm should match'
     89        );
     90        assert_true(
     91          encapsulatedKey.sharedKey.extractable,
     92          'Shared key should be extractable as specified'
     93        );
     94        assert_array_equals(
     95          encapsulatedKey.sharedKey.usages,
     96          config.usages,
     97          'Shared key should have correct usages'
     98        );
     99 
    100        // Verify algorithm-specific properties
    101        if (config.algorithm.length) {
    102          assert_equals(
    103            encapsulatedKey.sharedKey.algorithm.length,
    104            config.algorithm.length,
    105            'Key length should be 256'
    106          );
    107        }
    108        if (config.algorithm.hash) {
    109          assert_equals(
    110            encapsulatedKey.sharedKey.algorithm.hash.name,
    111            config.algorithm.hash,
    112            'Hash algorithm should match'
    113          );
    114        }
    115 
    116        // Verify ciphertext length based on algorithm variant
    117        var expectedCiphertextLength;
    118        switch (algorithmName) {
    119          case 'ML-KEM-512':
    120            expectedCiphertextLength = 768;
    121            break;
    122          case 'ML-KEM-768':
    123            expectedCiphertextLength = 1088;
    124            break;
    125          case 'ML-KEM-1024':
    126            expectedCiphertextLength = 1568;
    127            break;
    128        }
    129        assert_equals(
    130          encapsulatedKey.ciphertext.byteLength,
    131          expectedCiphertextLength,
    132          'Ciphertext should be ' +
    133            expectedCiphertextLength +
    134            ' bytes for ' +
    135            algorithmName
    136        );
    137      }, algorithmName + ' encapsulateKey with ' + config.description);
    138 
    139      // Test decapsulateKey operation
    140      promise_test(async function (test) {
    141        // Generate a key pair for testing
    142        var keyPair = await subtle.generateKey({ name: algorithmName }, false, [
    143          'encapsulateKey',
    144          'decapsulateKey',
    145        ]);
    146 
    147        // First encapsulate to get ciphertext
    148        var encapsulatedKey = await subtle.encapsulateKey(
    149          { name: algorithmName },
    150          keyPair.publicKey,
    151          config.algorithm,
    152          true,
    153          config.usages
    154        );
    155 
    156        // Then decapsulate using the private key
    157        var decapsulatedKey = await subtle.decapsulateKey(
    158          { name: algorithmName },
    159          keyPair.privateKey,
    160          encapsulatedKey.ciphertext,
    161          config.algorithm,
    162          true,
    163          config.usages
    164        );
    165 
    166        assert_true(
    167          decapsulatedKey instanceof CryptoKey,
    168          'decapsulateKey should return a CryptoKey'
    169        );
    170        assert_equals(
    171          decapsulatedKey.type,
    172          'secret',
    173          'Decapsulated key should be secret type'
    174        );
    175        assert_equals(
    176          decapsulatedKey.algorithm.name,
    177          config.algorithm.name,
    178          'Decapsulated key algorithm should match'
    179        );
    180        assert_true(
    181          decapsulatedKey.extractable,
    182          'Decapsulated key should be extractable as specified'
    183        );
    184        assert_array_equals(
    185          decapsulatedKey.usages,
    186          config.usages,
    187          'Decapsulated key should have correct usages'
    188        );
    189 
    190        // Extract both keys and verify they are identical
    191        var originalKeyMaterial = await subtle.exportKey(
    192          'raw',
    193          encapsulatedKey.sharedKey
    194        );
    195        var decapsulatedKeyMaterial = await subtle.exportKey(
    196          'raw',
    197          decapsulatedKey
    198        );
    199 
    200        assert_true(
    201          equalBuffers(originalKeyMaterial, decapsulatedKeyMaterial),
    202          'Decapsulated key material should match original'
    203        );
    204 
    205        // Verify the key material is 32 bytes (256 bits)
    206        assert_equals(
    207          originalKeyMaterial.byteLength,
    208          32,
    209          'Shared key material should be 32 bytes'
    210        );
    211      }, algorithmName + ' decapsulateKey with ' + config.description);
    212 
    213      // Test round-trip compatibility
    214      promise_test(async function (test) {
    215        var keyPair = await subtle.generateKey({ name: algorithmName }, false, [
    216          'encapsulateKey',
    217          'decapsulateKey',
    218        ]);
    219 
    220        var encapsulatedKey = await subtle.encapsulateKey(
    221          { name: algorithmName },
    222          keyPair.publicKey,
    223          config.algorithm,
    224          true,
    225          config.usages
    226        );
    227 
    228        var decapsulatedKey = await subtle.decapsulateKey(
    229          { name: algorithmName },
    230          keyPair.privateKey,
    231          encapsulatedKey.ciphertext,
    232          config.algorithm,
    233          true,
    234          config.usages
    235        );
    236 
    237        // Verify keys have the same material
    238        var originalKeyMaterial = await subtle.exportKey(
    239          'raw',
    240          encapsulatedKey.sharedKey
    241        );
    242        var decapsulatedKeyMaterial = await subtle.exportKey(
    243          'raw',
    244          decapsulatedKey
    245        );
    246 
    247        assert_true(
    248          equalBuffers(originalKeyMaterial, decapsulatedKeyMaterial),
    249          'Encapsulated and decapsulated keys should have the same material'
    250        );
    251 
    252        // Test that the derived keys can actually be used for their intended purpose
    253        if (
    254          config.algorithm.name.startsWith('AES') &&
    255          config.usages.includes('encrypt')
    256        ) {
    257          await testAESOperation(
    258            encapsulatedKey.sharedKey,
    259            decapsulatedKey,
    260            config.algorithm
    261          );
    262        } else if (config.algorithm.name === 'HMAC') {
    263          await testHMACOperation(encapsulatedKey.sharedKey, decapsulatedKey);
    264        }
    265      }, algorithmName +
    266        ' encapsulateKey/decapsulateKey round-trip with ' +
    267        config.description);
    268    });
    269 
    270    // Test vector-based decapsulation for each shared key config
    271    sharedKeyConfigs.forEach(function (config) {
    272      promise_test(async function (test) {
    273        var vectors = ml_kem_vectors[algorithmName];
    274 
    275        // Import the private key from the vector's privateSeed
    276        var privateKey = await subtle.importKey(
    277          'raw-seed',
    278          vectors.privateSeed,
    279          { name: algorithmName },
    280          false,
    281          ['decapsulateKey']
    282        );
    283 
    284        // Decapsulate the sample ciphertext from the vectors to get a shared key
    285        var decapsulatedKey = await subtle.decapsulateKey(
    286          { name: algorithmName },
    287          privateKey,
    288          vectors.sampleCiphertext,
    289          config.algorithm,
    290          true,
    291          config.usages
    292        );
    293 
    294        assert_true(
    295          decapsulatedKey instanceof CryptoKey,
    296          'decapsulateKey should return a CryptoKey'
    297        );
    298        assert_equals(
    299          decapsulatedKey.type,
    300          'secret',
    301          'Decapsulated key should be secret type'
    302        );
    303        assert_equals(
    304          decapsulatedKey.algorithm.name,
    305          config.algorithm.name,
    306          'Decapsulated key algorithm should match'
    307        );
    308        assert_true(
    309          decapsulatedKey.extractable,
    310          'Decapsulated key should be extractable as specified'
    311        );
    312        assert_array_equals(
    313          decapsulatedKey.usages,
    314          config.usages,
    315          'Decapsulated key should have correct usages'
    316        );
    317 
    318        // Extract the key material and verify it matches the expected shared secret
    319        var keyMaterial = await subtle.exportKey('raw', decapsulatedKey);
    320        assert_equals(
    321          keyMaterial.byteLength,
    322          32,
    323          'Shared key material should be 32 bytes'
    324        );
    325        assert_true(
    326          equalBuffers(keyMaterial, vectors.expectedSharedSecret),
    327          "Decapsulated key material should match vector's expectedSharedSecret"
    328        );
    329 
    330        // Verify algorithm-specific properties
    331        if (config.algorithm.length) {
    332          assert_equals(
    333            decapsulatedKey.algorithm.length,
    334            config.algorithm.length,
    335            'Key length should be 256'
    336          );
    337        }
    338        if (config.algorithm.hash) {
    339          assert_equals(
    340            decapsulatedKey.algorithm.hash.name,
    341            config.algorithm.hash,
    342            'Hash algorithm should match'
    343          );
    344        }
    345      }, algorithmName +
    346        ' vector-based sampleCiphertext decapsulation with ' +
    347        config.description);
    348    });
    349  });
    350 }
    351 
    352 async function testAESOperation(key1, key2, algorithm) {
    353  var plaintext = new Uint8Array([
    354    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
    355  ]);
    356 
    357  if (algorithm.name === 'AES-GCM') {
    358    var iv = crypto.getRandomValues(new Uint8Array(12));
    359    var params = { name: 'AES-GCM', iv: iv };
    360 
    361    var ciphertext1 = await crypto.subtle.encrypt(params, key1, plaintext);
    362    var ciphertext2 = await crypto.subtle.encrypt(params, key2, plaintext);
    363 
    364    assert_true(
    365      equalBuffers(ciphertext1, ciphertext2),
    366      'AES-GCM encryption should produce identical results'
    367    );
    368 
    369    var decrypted1 = await crypto.subtle.decrypt(params, key1, ciphertext1);
    370    var decrypted2 = await crypto.subtle.decrypt(params, key2, ciphertext2);
    371 
    372    assert_true(
    373      equalBuffers(decrypted1, plaintext),
    374      'AES-GCM decryption should work with key1'
    375    );
    376    assert_true(
    377      equalBuffers(decrypted2, plaintext),
    378      'AES-GCM decryption should work with key2'
    379    );
    380  } else if (algorithm.name === 'AES-CBC') {
    381    var iv = crypto.getRandomValues(new Uint8Array(16));
    382    var params = { name: 'AES-CBC', iv: iv };
    383 
    384    var ciphertext1 = await crypto.subtle.encrypt(params, key1, plaintext);
    385    var ciphertext2 = await crypto.subtle.encrypt(params, key2, plaintext);
    386 
    387    assert_true(
    388      equalBuffers(ciphertext1, ciphertext2),
    389      'AES-CBC encryption should produce identical results'
    390    );
    391  } else if (algorithm.name === 'AES-CTR') {
    392    var counter = crypto.getRandomValues(new Uint8Array(16));
    393    var params = { name: 'AES-CTR', counter: counter, length: 128 };
    394 
    395    var ciphertext1 = await crypto.subtle.encrypt(params, key1, plaintext);
    396    var ciphertext2 = await crypto.subtle.encrypt(params, key2, plaintext);
    397 
    398    assert_true(
    399      equalBuffers(ciphertext1, ciphertext2),
    400      'AES-CTR encryption should produce identical results'
    401    );
    402  }
    403 }
    404 
    405 async function testHMACOperation(key1, key2) {
    406  var data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
    407 
    408  var signature1 = await crypto.subtle.sign({ name: 'HMAC' }, key1, data);
    409  var signature2 = await crypto.subtle.sign({ name: 'HMAC' }, key2, data);
    410 
    411  assert_true(
    412    equalBuffers(signature1, signature2),
    413    'HMAC signatures should be identical'
    414  );
    415 
    416  var verified1 = await crypto.subtle.verify(
    417    { name: 'HMAC' },
    418    key1,
    419    signature1,
    420    data
    421  );
    422  var verified2 = await crypto.subtle.verify(
    423    { name: 'HMAC' },
    424    key2,
    425    signature2,
    426    data
    427  );
    428 
    429  assert_true(verified1, 'HMAC verification should succeed with key1');
    430  assert_true(verified2, 'HMAC verification should succeed with key2');
    431 }
    432 
    433 // Helper function to compare two ArrayBuffers
    434 function equalBuffers(a, b) {
    435  if (a.byteLength !== b.byteLength) {
    436    return false;
    437  }
    438  var aBytes = new Uint8Array(a);
    439  var bBytes = new Uint8Array(b);
    440  for (var i = 0; i < a.byteLength; i++) {
    441    if (aBytes[i] !== bBytes[i]) {
    442      return false;
    443    }
    444  }
    445  return true;
    446 }
    447 
    448 define_key_tests();