mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-05-03 04:52:06 +02:00
Tests: Re-import WebCryptoAPI tests
Some test have changed name and some utilities have now expanded to accommodate new algorithms.
This commit is contained in:
committed by
Jelle Raaijmakers
parent
8a79792a58
commit
aa44d254a4
Notes:
github-actions[bot]
2025-12-10 20:30:16 +00:00
Author: https://github.com/tete17 Commit: https://github.com/LadybirdBrowser/ladybird/commit/aa44d254a46 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/6935 Reviewed-by: https://github.com/R-Goc Reviewed-by: https://github.com/gmta ✅
@@ -13,4 +13,4 @@ self.GLOBAL = {
|
||||
<script src="cfrg_curves_bits_fixtures.js"></script>
|
||||
<script src="cfrg_curves_bits.js"></script>
|
||||
<div id=log></div>
|
||||
<script src="../../WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve448.https.any.js"></script>
|
||||
<script src="../../WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js"></script>
|
||||
@@ -284,7 +284,7 @@ function run_test() {
|
||||
resolve(vector);
|
||||
});
|
||||
} else {
|
||||
return subtle.importKey("raw", vector.keyBuffer, {name: vector.algorithm.name}, false, usages)
|
||||
return subtle.importKey(vector.algorithm.name.toUpperCase() === "AES-OCB" ? "raw-secret" : "raw", vector.keyBuffer, {name: vector.algorithm.name}, false, usages)
|
||||
.then(function(key) {
|
||||
vector.key = key;
|
||||
return vector;
|
||||
|
||||
@@ -25,6 +25,8 @@ function run_test(algorithmNames) {
|
||||
{name: "AES-CTR", resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
|
||||
{name: "AES-CBC", resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
|
||||
{name: "AES-GCM", resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
|
||||
{name: "AES-OCB", resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
|
||||
{name: "ChaCha20-Poly1305", resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
|
||||
{name: "AES-KW", resultType: CryptoKey, usages: ["wrapKey", "unwrapKey"], mandatoryUsages: []},
|
||||
{name: "HMAC", resultType: CryptoKey, usages: ["sign", "verify"], mandatoryUsages: []},
|
||||
{name: "RSASSA-PKCS1-v1_5", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
|
||||
@@ -34,8 +36,16 @@ function run_test(algorithmNames) {
|
||||
{name: "ECDH", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]},
|
||||
{name: "Ed25519", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
|
||||
{name: "Ed448", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
|
||||
{name: "ML-DSA-44", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
|
||||
{name: "ML-DSA-65", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
|
||||
{name: "ML-DSA-87", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
|
||||
{name: "ML-KEM-512", resultType: "CryptoKeyPair", usages: ["decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"], mandatoryUsages: ["decapsulateBits", "decapsulateKey"]},
|
||||
{name: "ML-KEM-768", resultType: "CryptoKeyPair", usages: ["decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"], mandatoryUsages: ["decapsulateBits", "decapsulateKey"]},
|
||||
{name: "ML-KEM-1024", resultType: "CryptoKeyPair", usages: ["decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"], mandatoryUsages: ["decapsulateBits", "decapsulateKey"]},
|
||||
{name: "X25519", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]},
|
||||
{name: "X448", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]},
|
||||
{name: "KMAC128", resultType: CryptoKey, usages: ["sign", "verify"], mandatoryUsages: []},
|
||||
{name: "KMAC256", resultType: CryptoKey, usages: ["sign", "verify"], mandatoryUsages: []},
|
||||
];
|
||||
|
||||
var testVectors = [];
|
||||
|
||||
@@ -14,4 +14,4 @@ self.GLOBAL = {
|
||||
<script src="../util/helpers.js"></script>
|
||||
<script src="failures.js"></script>
|
||||
<div id=log></div>
|
||||
<script src="../../WebCryptoAPI/generateKey/failures_X448.https.any.js"></script>
|
||||
<script src="../../WebCryptoAPI/generateKey/failures_Ed448.tentative.https.any.js"></script>
|
||||
@@ -14,4 +14,4 @@ self.GLOBAL = {
|
||||
<script src="../util/helpers.js"></script>
|
||||
<script src="failures.js"></script>
|
||||
<div id=log></div>
|
||||
<script src="../../WebCryptoAPI/generateKey/failures_Ed448.https.any.js"></script>
|
||||
<script src="../../WebCryptoAPI/generateKey/failures_X448.tentative.https.any.js"></script>
|
||||
@@ -21,6 +21,8 @@ function run_test(algorithmNames, slowTest) {
|
||||
{name: "AES-CTR", resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
|
||||
{name: "AES-CBC", resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
|
||||
{name: "AES-GCM", resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
|
||||
{name: "AES-OCB", resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
|
||||
{name: "ChaCha20-Poly1305", resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
|
||||
{name: "AES-KW", resultType: CryptoKey, usages: ["wrapKey", "unwrapKey"], mandatoryUsages: []},
|
||||
{name: "HMAC", resultType: CryptoKey, usages: ["sign", "verify"], mandatoryUsages: []},
|
||||
{name: "RSASSA-PKCS1-v1_5", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
|
||||
@@ -30,8 +32,16 @@ function run_test(algorithmNames, slowTest) {
|
||||
{name: "ECDH", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]},
|
||||
{name: "Ed25519", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
|
||||
{name: "Ed448", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
|
||||
{name: "ML-DSA-44", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
|
||||
{name: "ML-DSA-65", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
|
||||
{name: "ML-DSA-87", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
|
||||
{name: "ML-KEM-512", resultType: "CryptoKeyPair", usages: ["decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"], mandatoryUsages: ["decapsulateBits", "decapsulateKey"]},
|
||||
{name: "ML-KEM-768", resultType: "CryptoKeyPair", usages: ["decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"], mandatoryUsages: ["decapsulateBits", "decapsulateKey"]},
|
||||
{name: "ML-KEM-1024", resultType: "CryptoKeyPair", usages: ["decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"], mandatoryUsages: ["decapsulateBits", "decapsulateKey"]},
|
||||
{name: "X25519", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]},
|
||||
{name: "X448", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]},
|
||||
{name: "KMAC128", resultType: CryptoKey, usages: ["sign", "verify"], mandatoryUsages: []},
|
||||
{name: "KMAC256", resultType: CryptoKey, usages: ["sign", "verify"], mandatoryUsages: []},
|
||||
];
|
||||
|
||||
var testVectors = [];
|
||||
@@ -74,20 +84,60 @@ function run_test(algorithmNames, slowTest) {
|
||||
assert_unreached("generateKey threw an unexpected error: " + err.toString());
|
||||
})
|
||||
.then(async function (result) {
|
||||
if (resultType === "CryptoKeyPair") {
|
||||
await Promise.all([
|
||||
subtle.exportKey('jwk', result.publicKey),
|
||||
// TODO: remove this block to enable ML-KEM JWK when its definition is done in IETF JOSE WG
|
||||
if (result.publicKey?.algorithm.name.startsWith('ML-KEM')) {
|
||||
const promises = [
|
||||
subtle.exportKey('spki', result.publicKey),
|
||||
result.publicKey.algorithm.name.startsWith('RSA') ? undefined : subtle.exportKey('raw', result.publicKey),
|
||||
...(extractable ? [
|
||||
subtle.exportKey('jwk', result.privateKey),
|
||||
subtle.exportKey('pkcs8', result.privateKey),
|
||||
] : [])
|
||||
]);
|
||||
extractable ? subtle.exportKey('pkcs8', result.privateKey) : undefined,
|
||||
subtle.exportKey('raw-public', result.publicKey),
|
||||
];
|
||||
if (extractable)
|
||||
promises.push(subtle.exportKey('raw-seed', result.privateKey));
|
||||
} else if (resultType === "CryptoKeyPair") {
|
||||
const promises = [
|
||||
subtle.exportKey('jwk', result.publicKey),
|
||||
extractable ? subtle.exportKey('jwk', result.privateKey) : undefined,
|
||||
subtle.exportKey('spki', result.publicKey),
|
||||
extractable ? subtle.exportKey('pkcs8', result.privateKey) : undefined,
|
||||
];
|
||||
|
||||
switch (result.publicKey.algorithm.name.substring(0, 2)) {
|
||||
case 'ML':
|
||||
promises.push(subtle.exportKey('raw-public', result.publicKey));
|
||||
if (extractable)
|
||||
promises.push(subtle.exportKey('raw-seed', result.privateKey));
|
||||
break;
|
||||
case 'SL':
|
||||
promises.push(subtle.exportKey('raw-public', result.publicKey));
|
||||
if (extractable)
|
||||
promises.push(subtle.exportKey('raw-private', result.privateKey));
|
||||
break;
|
||||
case 'EC':
|
||||
case 'Ed':
|
||||
case 'X2':
|
||||
case 'X4':
|
||||
promises.push(subtle.exportKey('raw', result.publicKey));
|
||||
break;
|
||||
case 'RS':
|
||||
break;
|
||||
default:
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
const [jwkPub, jwkPriv] = await Promise.all(promises);
|
||||
|
||||
if (extractable) {
|
||||
// Test that the JWK public key is a superset of the JWK private key.
|
||||
for (const [prop, value] of Object.entries(jwkPub)) {
|
||||
if (prop !== 'key_ops') {
|
||||
assert_equals(value, jwkPriv[prop], `Property ${prop} is equal in public and private JWK`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (extractable) {
|
||||
await Promise.all([
|
||||
subtle.exportKey('raw', result),
|
||||
subtle.exportKey(/cha|ocb|kmac/i.test(result.algorithm.name) ? 'raw-secret' : 'raw', result),
|
||||
subtle.exportKey('jwk', result),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -15,4 +15,4 @@ self.GLOBAL = {
|
||||
<script src="../../common/subset-tests.js"></script>
|
||||
<script src="successes.js"></script>
|
||||
<div id=log></div>
|
||||
<script src="../../WebCryptoAPI/generateKey/successes_Ed448.https.any.js"></script>
|
||||
<script src="../../WebCryptoAPI/generateKey/successes_Ed448.tentative.https.any.js"></script>
|
||||
@@ -15,4 +15,4 @@ self.GLOBAL = {
|
||||
<script src="../../common/subset-tests.js"></script>
|
||||
<script src="successes.js"></script>
|
||||
<div id=log></div>
|
||||
<script src="../../WebCryptoAPI/generateKey/successes_X448.https.any.js"></script>
|
||||
<script src="../../WebCryptoAPI/generateKey/successes_X448.tentative.https.any.js"></script>
|
||||
@@ -20,11 +20,11 @@ function getMismatchedJWKKeyData(algorithm) {
|
||||
}
|
||||
|
||||
function getMismatchedKtyField(algorithm) {
|
||||
return mismatchedKtyField[algorithm.name];
|
||||
return mismatchedKtyField[algorithm.namedCurve];
|
||||
}
|
||||
|
||||
function getMismatchedCrvField(algorithm) {
|
||||
return mismatchedCrvField[algorithm.name];
|
||||
return mismatchedCrvField[algorithm.namedCurve];
|
||||
}
|
||||
|
||||
var validKeyData = {
|
||||
|
||||
@@ -19,6 +19,12 @@ function run_test(algorithmNames) {
|
||||
|
||||
var allTestVectors = [ // Parameters that should work for importKey / exportKey
|
||||
{name: "Ed25519", privateUsages: ["sign"], publicUsages: ["verify"]},
|
||||
{name: "ML-DSA-44", privateUsages: ["sign"], publicUsages: ["verify"]},
|
||||
{name: "ML-DSA-65", privateUsages: ["sign"], publicUsages: ["verify"]},
|
||||
{name: "ML-DSA-87", privateUsages: ["sign"], publicUsages: ["verify"]},
|
||||
{name: "ML-KEM-512", privateUsages: ["decapsulateKey", "decapsulateBits"], publicUsages: ["encapsulateKey", "encapsulateBits"]},
|
||||
{name: "ML-KEM-768", privateUsages: ["decapsulateKey", "decapsulateBits"], publicUsages: ["encapsulateKey", "encapsulateBits"]},
|
||||
{name: "ML-KEM-1024", privateUsages: ["decapsulateKey", "decapsulateBits"], publicUsages: ["encapsulateKey", "encapsulateBits"]},
|
||||
{name: "Ed448", privateUsages: ["sign"], publicUsages: ["verify"]},
|
||||
{name: "ECDSA", privateUsages: ["sign"], publicUsages: ["verify"]},
|
||||
{name: "X25519", privateUsages: ["deriveKey", "deriveBits"], publicUsages: []},
|
||||
@@ -43,7 +49,7 @@ function run_test(algorithmNames) {
|
||||
|
||||
var jwk_label = "";
|
||||
if (format === "jwk")
|
||||
jwk_label = data.d === undefined ? " (public) " : "(private)";
|
||||
jwk_label = isPublicKey(data) ? " (public) " : "(private)";
|
||||
|
||||
var result = "(" +
|
||||
objectToString(format) + jwk_label + ", " +
|
||||
@@ -101,18 +107,22 @@ function run_test(algorithmNames) {
|
||||
}
|
||||
|
||||
function validUsages(usages, format, data) {
|
||||
if (format === 'spki' || format === 'raw') return usages.publicUsages
|
||||
if (format === 'pkcs8') return usages.privateUsages
|
||||
if (format === 'spki' || format === 'raw' || format === 'raw-public') return usages.publicUsages
|
||||
if (format === 'pkcs8' || format === 'raw-private' || format === 'raw-seed') return usages.privateUsages
|
||||
if (format === 'jwk') {
|
||||
if (data === undefined)
|
||||
return [];
|
||||
return data.d === undefined ? usages.publicUsages : usages.privateUsages;
|
||||
return isPublicKey(data) ? usages.publicUsages : usages.privateUsages;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function isPublicKey(data) {
|
||||
return data.d === undefined && data.priv === undefined;
|
||||
}
|
||||
|
||||
function isPrivateKey(data) {
|
||||
return data.d !== undefined;
|
||||
return !isPublicKey(data);
|
||||
}
|
||||
|
||||
// Now test for properly handling errors
|
||||
@@ -243,7 +253,7 @@ function run_test(algorithmNames) {
|
||||
allAlgorithmSpecifiersFor(name).forEach(function(algorithm) {
|
||||
getValidKeyData(algorithm).forEach(function(test) {
|
||||
if (test.format === "jwk") {
|
||||
var data = {crv: test.data.crv, kty: test.data.kty, d: test.data.d, x: test.data.x, d: test.data.d};
|
||||
var data = {crv: test.data.crv, kty: test.data.kty, d: test.data.d, x: test.data.x, y: test.data.y};
|
||||
data.use = "invalid";
|
||||
var usages = validUsages(vector, 'jwk', test.data);
|
||||
if (usages.length !== 0)
|
||||
|
||||
@@ -15,4 +15,4 @@ self.GLOBAL = {
|
||||
<script src="okp_importKey_fixtures.js"></script>
|
||||
<script src="okp_importKey.js"></script>
|
||||
<div id=log></div>
|
||||
<script src="../../WebCryptoAPI/import_export/okp_importKey_X448.https.any.js"></script>
|
||||
<script src="../../WebCryptoAPI/import_export/okp_importKey_X448.tentative.https.any.js"></script>
|
||||
@@ -15,4 +15,4 @@ self.GLOBAL = {
|
||||
<script src="okp_importKey_failures_fixtures.js"></script>
|
||||
<script src="importKey_failures.js"></script>
|
||||
<div id=log></div>
|
||||
<script src="../../WebCryptoAPI/import_export/okp_importKey_failures_X448.https.any.js"></script>
|
||||
<script src="../../WebCryptoAPI/import_export/okp_importKey_failures_X448.tentative.https.any.js"></script>
|
||||
@@ -2,27 +2,27 @@
|
||||
// helper functions that generate all possible test parameters for
|
||||
// different situations.
|
||||
function getValidKeyData(algorithm) {
|
||||
return validKeyData[algorithm.name];
|
||||
return validKeyData[algorithm.name || algorithm];
|
||||
}
|
||||
|
||||
function getBadKeyLengthData(algorithm) {
|
||||
return badKeyLengthData[algorithm.name];
|
||||
return badKeyLengthData[algorithm.name || algorithm];
|
||||
}
|
||||
|
||||
function getMissingJWKFieldKeyData(algorithm) {
|
||||
return missingJWKFieldKeyData[algorithm.name];
|
||||
return missingJWKFieldKeyData[algorithm.name || algorithm];
|
||||
}
|
||||
|
||||
function getMismatchedJWKKeyData(algorithm) {
|
||||
return mismatchedJWKKeyData[algorithm.name];
|
||||
return mismatchedJWKKeyData[algorithm.name || algorithm];
|
||||
}
|
||||
|
||||
function getMismatchedKtyField(algorithm) {
|
||||
return mismatchedKtyField[algorithm.name];
|
||||
return mismatchedKtyField[algorithm.name || algorithm];
|
||||
}
|
||||
|
||||
function getMismatchedCrvField(algorithm) {
|
||||
return mismatchedCrvField[algorithm.name];
|
||||
return mismatchedCrvField[algorithm.name || algorithm];
|
||||
}
|
||||
|
||||
var validKeyData = {
|
||||
@@ -432,7 +432,7 @@ var mismatchedKtyField = {
|
||||
// The 'kty' field doesn't match the key algorithm.
|
||||
var mismatchedCrvField = {
|
||||
"Ed25519": "X25519",
|
||||
"X25519": "Ed448",
|
||||
"Ed448": "X25519",
|
||||
"X448": "Ed25519",
|
||||
"X25519": "Ed25519",
|
||||
"Ed448": "X448",
|
||||
"X448": "Ed448",
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ self.GLOBAL = {
|
||||
<script src="../../resources/testharness.js"></script>
|
||||
<script src="../../resources/testharnessreport.js"></script>
|
||||
<script src="../util/helpers.js"></script>
|
||||
<script src="symmetric_importKey.js"></script>
|
||||
<div id=log></div>
|
||||
<script src="../../WebCryptoAPI/import_export/symmetric_importKey.https.any.js"></script>
|
||||
|
||||
@@ -1,222 +1,12 @@
|
||||
// META: title=WebCryptoAPI: importKey() for symmetric keys
|
||||
// META: timeout=long
|
||||
// META: script=../util/helpers.js
|
||||
// META: script=symmetric_importKey.js
|
||||
|
||||
// Test importKey and exportKey for non-PKC algorithms. Only "happy paths" are
|
||||
// currently tested - those where the operation should succeed.
|
||||
|
||||
var subtle = crypto.subtle;
|
||||
|
||||
// keying material for algorithms that can use any bit string.
|
||||
var rawKeyData = [
|
||||
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
|
||||
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
||||
17, 18, 19, 20, 21, 22, 23, 24]),
|
||||
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
||||
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32])
|
||||
];
|
||||
|
||||
// combinations of algorithms, usages, parameters, and formats to test
|
||||
var testVectors = [
|
||||
{name: "AES-CTR", legalUsages: ["encrypt", "decrypt"], extractable: [true, false], formats: ["raw", "jwk"]},
|
||||
{name: "AES-CBC", legalUsages: ["encrypt", "decrypt"], extractable: [true, false], formats: ["raw", "jwk"]},
|
||||
{name: "AES-GCM", legalUsages: ["encrypt", "decrypt"], extractable: [true, false], formats: ["raw", "jwk"]},
|
||||
{name: "AES-KW", legalUsages: ["wrapKey", "unwrapKey"], extractable: [true, false], formats: ["raw", "jwk"]},
|
||||
{name: "HMAC", hash: "SHA-1", legalUsages: ["sign", "verify"], extractable: [false], formats: ["raw", "jwk"]},
|
||||
{name: "HMAC", hash: "SHA-256", legalUsages: ["sign", "verify"], extractable: [false], formats: ["raw", "jwk"]},
|
||||
{name: "HMAC", hash: "SHA-384", legalUsages: ["sign", "verify"], extractable: [false], formats: ["raw", "jwk"]},
|
||||
{name: "HMAC", hash: "SHA-512", legalUsages: ["sign", "verify"], extractable: [false], formats: ["raw", "jwk"]},
|
||||
{name: "HKDF", legalUsages: ["deriveBits", "deriveKey"], extractable: [false], formats: ["raw"]},
|
||||
{name: "PBKDF2", legalUsages: ["deriveBits", "deriveKey"], extractable: [false], formats: ["raw"]}
|
||||
];
|
||||
|
||||
|
||||
|
||||
// TESTS ARE HERE:
|
||||
// Test every test vector, along with all available key data
|
||||
testVectors.forEach(function(vector) {
|
||||
var algorithm = {name: vector.name};
|
||||
if ("hash" in vector) {
|
||||
algorithm.hash = vector.hash;
|
||||
}
|
||||
|
||||
rawKeyData.forEach(function(keyData) {
|
||||
// Try each legal value of the extractable parameter
|
||||
vector.extractable.forEach(function(extractable) {
|
||||
vector.formats.forEach(function(format) {
|
||||
var data = keyData;
|
||||
if (format === "jwk") {
|
||||
data = jwkData(keyData, algorithm);
|
||||
}
|
||||
// Generate all combinations of valid usages for testing
|
||||
allValidUsages(vector.legalUsages).forEach(function(usages) {
|
||||
testFormat(format, algorithm, data, keyData.length * 8, usages, extractable);
|
||||
});
|
||||
testEmptyUsages(format, algorithm, data, keyData.length * 8, extractable);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
function hasLength(algorithm) {
|
||||
return algorithm.name === 'HMAC' || algorithm.name.startsWith('AES');
|
||||
}
|
||||
|
||||
// Test importKey with a given key format and other parameters. If
|
||||
// extrable is true, export the key and verify that it matches the input.
|
||||
function testFormat(format, algorithm, keyData, keySize, usages, extractable) {
|
||||
promise_test(function(test) {
|
||||
return subtle.importKey(format, keyData, algorithm, extractable, usages).
|
||||
then(function(key) {
|
||||
assert_equals(key.constructor, CryptoKey, "Imported a CryptoKey object");
|
||||
assert_goodCryptoKey(key, hasLength(key.algorithm) ? { length: keySize, ...algorithm } : algorithm, extractable, usages, 'secret');
|
||||
if (!extractable) {
|
||||
return;
|
||||
}
|
||||
|
||||
return subtle.exportKey(format, key).
|
||||
then(function(result) {
|
||||
if (format !== "jwk") {
|
||||
assert_true(equalBuffers(keyData, result), "Round trip works");
|
||||
} else {
|
||||
assert_true(equalJwk(keyData, result), "Round trip works");
|
||||
}
|
||||
}, function(err) {
|
||||
assert_unreached("Threw an unexpected error: " + err.toString());
|
||||
});
|
||||
}, function(err) {
|
||||
assert_unreached("Threw an unexpected error: " + err.toString());
|
||||
});
|
||||
}, "Good parameters: " + keySize.toString() + " bits " + parameterString(format, keyData, algorithm, extractable, usages));
|
||||
}
|
||||
|
||||
// Test importKey with a given key format and other parameters but with empty usages.
|
||||
// Should fail with SyntaxError
|
||||
function testEmptyUsages(format, algorithm, keyData, keySize, extractable) {
|
||||
const usages = [];
|
||||
promise_test(function(test) {
|
||||
return subtle.importKey(format, keyData, algorithm, extractable, usages).
|
||||
then(function(key) {
|
||||
assert_unreached("importKey succeeded but should have failed with SyntaxError");
|
||||
}, function(err) {
|
||||
assert_equals(err.name, "SyntaxError", "Should throw correct error, not " + err.name + ": " + err.message);
|
||||
});
|
||||
}, "Empty Usages: " + keySize.toString() + " bits " + parameterString(format, keyData, algorithm, extractable, usages));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Helper methods follow:
|
||||
|
||||
// Are two array buffers the same?
|
||||
function equalBuffers(a, b) {
|
||||
if (a.byteLength !== b.byteLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var aBytes = new Uint8Array(a);
|
||||
var bBytes = new Uint8Array(b);
|
||||
|
||||
for (var i=0; i<a.byteLength; i++) {
|
||||
if (aBytes[i] !== bBytes[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Are two Jwk objects "the same"? That is, does the object returned include
|
||||
// matching values for each property that was expected? It's okay if the
|
||||
// returned object has extra methods; they aren't checked.
|
||||
function equalJwk(expected, got) {
|
||||
var fields = Object.keys(expected);
|
||||
var fieldName;
|
||||
|
||||
for(var i=0; i<fields.length; i++) {
|
||||
fieldName = fields[i];
|
||||
if (!(fieldName in got)) {
|
||||
return false;
|
||||
}
|
||||
if (expected[fieldName] !== got[fieldName]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Build minimal Jwk objects from raw key data and algorithm specifications
|
||||
function jwkData(keyData, algorithm) {
|
||||
var result = {
|
||||
kty: "oct",
|
||||
k: byteArrayToUnpaddedBase64(keyData)
|
||||
};
|
||||
|
||||
if (algorithm.name.substring(0, 3) === "AES") {
|
||||
result.alg = "A" + (8 * keyData.byteLength).toString() + algorithm.name.substring(4);
|
||||
} else if (algorithm.name === "HMAC") {
|
||||
result.alg = "HS" + algorithm.hash.substring(4);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Jwk format wants Base 64 without the typical padding at the end.
|
||||
function byteArrayToUnpaddedBase64(byteArray){
|
||||
var binaryString = "";
|
||||
for (var i=0; i<byteArray.byteLength; i++){
|
||||
binaryString += String.fromCharCode(byteArray[i]);
|
||||
}
|
||||
var base64String = btoa(binaryString);
|
||||
|
||||
return base64String.replace(/=/g, "");
|
||||
}
|
||||
|
||||
// Convert method parameters to a string to uniquely name each test
|
||||
function parameterString(format, data, algorithm, extractable, usages) {
|
||||
var result = "(" +
|
||||
objectToString(format) + ", " +
|
||||
objectToString(data) + ", " +
|
||||
objectToString(algorithm) + ", " +
|
||||
objectToString(extractable) + ", " +
|
||||
objectToString(usages) +
|
||||
")";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Character representation of any object we may use as a parameter.
|
||||
function objectToString(obj) {
|
||||
var keyValuePairs = [];
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return "[" + obj.map(function(elem){return objectToString(elem);}).join(", ") + "]";
|
||||
} else if (typeof obj === "object") {
|
||||
Object.keys(obj).sort().forEach(function(keyName) {
|
||||
keyValuePairs.push(keyName + ": " + objectToString(obj[keyName]));
|
||||
});
|
||||
return "{" + keyValuePairs.join(", ") + "}";
|
||||
} else if (typeof obj === "undefined") {
|
||||
return "undefined";
|
||||
} else {
|
||||
return obj.toString();
|
||||
}
|
||||
|
||||
var keyValuePairs = [];
|
||||
|
||||
Object.keys(obj).sort().forEach(function(keyName) {
|
||||
var value = obj[keyName];
|
||||
if (typeof value === "object") {
|
||||
value = objectToString(value);
|
||||
} else if (typeof value === "array") {
|
||||
value = "[" + value.map(function(elem){return objectToString(elem);}).join(", ") + "]";
|
||||
} else {
|
||||
value = value.toString();
|
||||
}
|
||||
|
||||
keyValuePairs.push(keyName + ": " + value);
|
||||
});
|
||||
|
||||
return "{" + keyValuePairs.join(", ") + "}";
|
||||
}
|
||||
runTests("AES-CTR");
|
||||
runTests("AES-CBC");
|
||||
runTests("AES-GCM");
|
||||
runTests("AES-KW");
|
||||
runTests("HMAC");
|
||||
runTests("HKDF");
|
||||
runTests("PBKDF2");
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
// Test importKey and exportKey for non-PKC algorithms. Only "happy paths" are
|
||||
// currently tested - those where the operation should succeed.
|
||||
|
||||
|
||||
function runTests(algorithmName) {
|
||||
var subtle = crypto.subtle;
|
||||
|
||||
// keying material for algorithms that can use any bit string.
|
||||
var rawKeyData = [
|
||||
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
|
||||
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
||||
17, 18, 19, 20, 21, 22, 23, 24]),
|
||||
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
||||
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32])
|
||||
];
|
||||
|
||||
// combinations of algorithms, usages, parameters, and formats to test
|
||||
var testVectors = [
|
||||
{name: "AES-CTR", legalUsages: ["encrypt", "decrypt"], extractable: [true, false], formats: ["raw", "jwk"]},
|
||||
{name: "AES-CBC", legalUsages: ["encrypt", "decrypt"], extractable: [true, false], formats: ["raw", "jwk"]},
|
||||
{name: "AES-GCM", legalUsages: ["encrypt", "decrypt"], extractable: [true, false], formats: ["raw", "jwk"]},
|
||||
{name: "AES-KW", legalUsages: ["wrapKey", "unwrapKey"], extractable: [true, false], formats: ["raw", "jwk"]},
|
||||
{name: "HMAC", hash: "SHA-1", legalUsages: ["sign", "verify"], extractable: [true, false], formats: ["raw", "jwk"]},
|
||||
{name: "HMAC", hash: "SHA-256", legalUsages: ["sign", "verify"], extractable: [true, false], formats: ["raw", "jwk"]},
|
||||
{name: "HMAC", hash: "SHA-384", legalUsages: ["sign", "verify"], extractable: [true, false], formats: ["raw", "jwk"]},
|
||||
{name: "HMAC", hash: "SHA-512", legalUsages: ["sign", "verify"], extractable: [true, false], formats: ["raw", "jwk"]},
|
||||
{name: "HKDF", legalUsages: ["deriveBits", "deriveKey"], extractable: [false], formats: ["raw"]},
|
||||
{name: "PBKDF2", legalUsages: ["deriveBits", "deriveKey"], extractable: [false], formats: ["raw"]},
|
||||
{name: "Argon2i", legalUsages: ["deriveBits", "deriveKey"], extractable: [false], formats: ["raw-secret"]},
|
||||
{name: "Argon2d", legalUsages: ["deriveBits", "deriveKey"], extractable: [false], formats: ["raw-secret"]},
|
||||
{name: "Argon2id", legalUsages: ["deriveBits", "deriveKey"], extractable: [false], formats: ["raw-secret"]},
|
||||
{name: "AES-OCB", legalUsages: ["encrypt", "decrypt"], extractable: [true, false], formats: ["raw-secret", "jwk"]},
|
||||
{name: "ChaCha20-Poly1305", legalUsages: ["encrypt", "decrypt"], extractable: [true, false], formats: ["raw-secret", "jwk"]},
|
||||
{name: "KMAC128", legalUsages: ["sign", "verify"], extractable: [true, false], formats: ["raw-secret", "jwk"]},
|
||||
{name: "KMAC256", legalUsages: ["sign", "verify"], extractable: [true, false], formats: ["raw-secret", "jwk"]},
|
||||
];
|
||||
|
||||
|
||||
|
||||
// TESTS ARE HERE:
|
||||
// Test every test vector, along with all available key data
|
||||
testVectors.filter(({ name }) => name === algorithmName).forEach(function(vector) {
|
||||
var algorithm = {name: vector.name};
|
||||
if ("hash" in vector) {
|
||||
algorithm.hash = vector.hash;
|
||||
}
|
||||
|
||||
rawKeyData.forEach(function(keyData) {
|
||||
if (vector.name === 'ChaCha20-Poly1305' && keyData.byteLength !== 32) return;
|
||||
// Try each legal value of the extractable parameter
|
||||
vector.extractable.forEach(function(extractable) {
|
||||
vector.formats.forEach(function(format) {
|
||||
var data = keyData;
|
||||
if (format === "jwk") {
|
||||
data = jwkData(keyData, algorithm);
|
||||
}
|
||||
// Generate all combinations of valid usages for testing
|
||||
allValidUsages(vector.legalUsages).forEach(function(usages) {
|
||||
testFormat(format, algorithm, data, keyData.length * 8, usages, extractable);
|
||||
});
|
||||
testEmptyUsages(format, algorithm, data, keyData.length * 8, extractable);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
function hasLength(algorithm) {
|
||||
return algorithm.name === 'HMAC' || algorithm.name.startsWith('AES') || algorithm.name.startsWith('KMAC');
|
||||
}
|
||||
|
||||
// Test importKey with a given key format and other parameters. If
|
||||
// extrable is true, export the key and verify that it matches the input.
|
||||
function testFormat(format, algorithm, keyData, keySize, usages, extractable) {
|
||||
promise_test(function(test) {
|
||||
return subtle.importKey(format, keyData, algorithm, extractable, usages).
|
||||
then(function(key) {
|
||||
assert_equals(key.constructor, CryptoKey, "Imported a CryptoKey object");
|
||||
assert_goodCryptoKey(key, hasLength(key.algorithm) ? { length: keySize, ...algorithm } : algorithm, extractable, usages, 'secret');
|
||||
if (!extractable) {
|
||||
return;
|
||||
}
|
||||
|
||||
return subtle.exportKey(format, key).
|
||||
then(function(result) {
|
||||
if (format !== "jwk") {
|
||||
assert_true(equalBuffers(keyData, result), "Round trip works");
|
||||
} else {
|
||||
assert_true(equalJwk(keyData, result), "Round trip works");
|
||||
}
|
||||
}, function(err) {
|
||||
assert_unreached("Threw an unexpected error: " + err.toString());
|
||||
});
|
||||
}, function(err) {
|
||||
assert_unreached("Threw an unexpected error: " + err.toString());
|
||||
});
|
||||
}, "Good parameters: " + keySize.toString() + " bits " + parameterString(format, keyData, algorithm, extractable, usages));
|
||||
}
|
||||
|
||||
// Test importKey with a given key format and other parameters but with empty usages.
|
||||
// Should fail with SyntaxError
|
||||
function testEmptyUsages(format, algorithm, keyData, keySize, extractable) {
|
||||
const usages = [];
|
||||
promise_test(function(test) {
|
||||
return subtle.importKey(format, keyData, algorithm, extractable, usages).
|
||||
then(function(key) {
|
||||
assert_unreached("importKey succeeded but should have failed with SyntaxError");
|
||||
}, function(err) {
|
||||
assert_equals(err.name, "SyntaxError", "Should throw correct error, not " + err.name + ": " + err.message);
|
||||
});
|
||||
}, "Empty Usages: " + keySize.toString() + " bits " + parameterString(format, keyData, algorithm, extractable, usages));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Helper methods follow:
|
||||
|
||||
// Are two array buffers the same?
|
||||
function equalBuffers(a, b) {
|
||||
if (a.byteLength !== b.byteLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var aBytes = new Uint8Array(a);
|
||||
var bBytes = new Uint8Array(b);
|
||||
|
||||
for (var i=0; i<a.byteLength; i++) {
|
||||
if (aBytes[i] !== bBytes[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Are two Jwk objects "the same"? That is, does the object returned include
|
||||
// matching values for each property that was expected? It's okay if the
|
||||
// returned object has extra methods; they aren't checked.
|
||||
function equalJwk(expected, got) {
|
||||
var fields = Object.keys(expected);
|
||||
var fieldName;
|
||||
|
||||
for(var i=0; i<fields.length; i++) {
|
||||
fieldName = fields[i];
|
||||
if (!(fieldName in got)) {
|
||||
return false;
|
||||
}
|
||||
if (expected[fieldName] !== got[fieldName]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Build minimal Jwk objects from raw key data and algorithm specifications
|
||||
function jwkData(keyData, algorithm) {
|
||||
var result = {
|
||||
kty: "oct",
|
||||
k: byteArrayToUnpaddedBase64(keyData)
|
||||
};
|
||||
|
||||
if (algorithm.name.substring(0, 3) === "AES") {
|
||||
result.alg = "A" + (8 * keyData.byteLength).toString() + algorithm.name.substring(4);
|
||||
} else if (algorithm.name === "HMAC") {
|
||||
result.alg = "HS" + algorithm.hash.substring(4);
|
||||
} else if (algorithm.name.startsWith("KMAC")) {
|
||||
result.alg = "K" + algorithm.name.substring(4);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Jwk format wants Base 64 without the typical padding at the end.
|
||||
function byteArrayToUnpaddedBase64(byteArray){
|
||||
var binaryString = "";
|
||||
for (var i=0; i<byteArray.byteLength; i++){
|
||||
binaryString += String.fromCharCode(byteArray[i]);
|
||||
}
|
||||
var base64String = btoa(binaryString);
|
||||
|
||||
return base64String.replace(/=/g, "");
|
||||
}
|
||||
|
||||
// Convert method parameters to a string to uniquely name each test
|
||||
function parameterString(format, data, algorithm, extractable, usages) {
|
||||
var result = "(" +
|
||||
objectToString(format) + ", " +
|
||||
objectToString(data) + ", " +
|
||||
objectToString(algorithm) + ", " +
|
||||
objectToString(extractable) + ", " +
|
||||
objectToString(usages) +
|
||||
")";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Character representation of any object we may use as a parameter.
|
||||
function objectToString(obj) {
|
||||
var keyValuePairs = [];
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return "[" + obj.map(function(elem){return objectToString(elem);}).join(", ") + "]";
|
||||
} else if (typeof obj === "object") {
|
||||
Object.keys(obj).sort().forEach(function(keyName) {
|
||||
keyValuePairs.push(keyName + ": " + objectToString(obj[keyName]));
|
||||
});
|
||||
return "{" + keyValuePairs.join(", ") + "}";
|
||||
} else if (typeof obj === "undefined") {
|
||||
return "undefined";
|
||||
} else {
|
||||
return obj.toString();
|
||||
}
|
||||
|
||||
var keyValuePairs = [];
|
||||
|
||||
Object.keys(obj).sort().forEach(function(keyName) {
|
||||
var value = obj[keyName];
|
||||
if (typeof value === "object") {
|
||||
value = objectToString(value);
|
||||
} else if (typeof value === "array") {
|
||||
value = "[" + value.map(function(elem){return objectToString(elem);}).join(", ") + "]";
|
||||
} else {
|
||||
value = value.toString();
|
||||
}
|
||||
|
||||
keyValuePairs.push(keyName + ": " + value);
|
||||
});
|
||||
|
||||
return "{" + keyValuePairs.join(", ") + "}";
|
||||
}
|
||||
}
|
||||
@@ -14,4 +14,4 @@ self.GLOBAL = {
|
||||
<script src="eddsa_vectors.js"></script>
|
||||
<script src="eddsa.js"></script>
|
||||
<div id=log></div>
|
||||
<script src="../../WebCryptoAPI/sign_verify/eddsa_curve448.https.any.js"></script>
|
||||
<script src="../../WebCryptoAPI/sign_verify/eddsa_curve448.tentative.https.any.js"></script>
|
||||
@@ -24,7 +24,20 @@ var registeredAlgorithmNames = [
|
||||
"Ed25519",
|
||||
"Ed448",
|
||||
"X25519",
|
||||
"X448"
|
||||
"X448",
|
||||
"ML-DSA-44",
|
||||
"ML-DSA-65",
|
||||
"ML-DSA-87",
|
||||
"ML-KEM-512",
|
||||
"ML-KEM-768",
|
||||
"ML-KEM-1024",
|
||||
"ChaCha20-Poly1305",
|
||||
"Argon2i",
|
||||
"Argon2d",
|
||||
"Argon2id",
|
||||
"AES-OCB",
|
||||
"KMAC128",
|
||||
"KMAC256",
|
||||
];
|
||||
|
||||
|
||||
@@ -93,6 +106,10 @@ function objectToString(obj) {
|
||||
// Is key a CryptoKey object with correct algorithm, extractable, and usages?
|
||||
// Is it a secret, private, or public kind of key?
|
||||
function assert_goodCryptoKey(key, algorithm, extractable, usages, kind) {
|
||||
if (typeof algorithm === "string") {
|
||||
algorithm = { name: algorithm };
|
||||
}
|
||||
|
||||
var correctUsages = [];
|
||||
|
||||
var registeredAlgorithmName;
|
||||
@@ -120,6 +137,15 @@ function assert_goodCryptoKey(key, algorithm, extractable, usages, kind) {
|
||||
default:
|
||||
assert_unreached("Unrecognized hash");
|
||||
}
|
||||
} else if (key.algorithm.name.toUpperCase().startsWith("KMAC") && algorithm.length === undefined) {
|
||||
switch (key.algorithm.name.toUpperCase()) {
|
||||
case 'KMAC128':
|
||||
assert_equals(key.algorithm.length, 128, "Correct length");
|
||||
break;
|
||||
case 'KMAC256':
|
||||
assert_equals(key.algorithm.length, 256, "Correct length");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
assert_equals(key.algorithm.length, algorithm.length, "Correct length");
|
||||
}
|
||||
@@ -135,13 +161,13 @@ function assert_goodCryptoKey(key, algorithm, extractable, usages, kind) {
|
||||
// only a single key. The publicKey and privateKey portions of a key pair
|
||||
// recognize only some of the usages appropriate for a key pair.
|
||||
if (key.type === "public") {
|
||||
["encrypt", "verify", "wrapKey"].forEach(function(usage) {
|
||||
["encrypt", "verify", "wrapKey", "encapsulateBits", "encapsulateKey"].forEach(function(usage) {
|
||||
if (usages.includes(usage)) {
|
||||
correctUsages.push(usage);
|
||||
}
|
||||
});
|
||||
} else if (key.type === "private") {
|
||||
["decrypt", "sign", "unwrapKey", "deriveKey", "deriveBits"].forEach(function(usage) {
|
||||
["decrypt", "sign", "unwrapKey", "deriveKey", "deriveBits", "decapsulateBits", "decapsulateKey"].forEach(function(usage) {
|
||||
if (usages.includes(usage)) {
|
||||
correctUsages.push(usage);
|
||||
}
|
||||
@@ -202,7 +228,16 @@ function allAlgorithmSpecifiersFor(algorithmName) {
|
||||
curves.forEach(function(curveName) {
|
||||
results.push({name: algorithmName, namedCurve: curveName});
|
||||
});
|
||||
} else if (algorithmName.toUpperCase().substring(0, 1) === "X" || algorithmName.toUpperCase().substring(0, 2) === "ED") {
|
||||
} else if (algorithmName.toUpperCase().startsWith("KMAC")) {
|
||||
[
|
||||
{length: 128},
|
||||
{length: 160},
|
||||
{length: 256},
|
||||
].forEach(function(hashAlgorithm) {
|
||||
results.push({name: algorithmName, ...hashAlgorithm});
|
||||
});
|
||||
} else {
|
||||
results.push(algorithmName);
|
||||
results.push({ name: algorithmName });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user