LibWeb: Implement wrapKey and unwrapKey methods

This implements the last WebCryptoAPI methods `wrapKey` and `unwrapKey`.
Most of the functionality is already there because they rely on
`encrypt` and `decrypt`. The only test failures are for `AES-GCM` which
is not implemented yet.
This commit is contained in:
devgianlu 2024-12-14 12:23:52 +01:00 committed by Andreas Kling
commit 584cbcf3ef
Notes: github-actions[bot] 2024-12-16 10:35:53 +00:00
8 changed files with 1221 additions and 2 deletions

View file

@ -6,9 +6,11 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ByteBuffer.h>
#include <AK/QuickSort.h>
#include <LibCrypto/Hash/HashManager.h>
#include <LibJS/Runtime/ArrayBuffer.h>
#include <LibJS/Runtime/JSONObject.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/SubtleCryptoPrototype.h>
@ -744,6 +746,293 @@ JS::ThrowCompletionOr<GC::Ref<WebIDL::Promise>> SubtleCrypto::derive_key(Algorit
return promise;
}
// https://w3c.github.io/webcrypto/#SubtleCrypto-method-wrapKey
JS::ThrowCompletionOr<GC::Ref<WebIDL::Promise>> SubtleCrypto::wrap_key(Bindings::KeyFormat format, GC::Ref<CryptoKey> key, GC::Ref<CryptoKey> wrapping_key, AlgorithmIdentifier algorithm)
{
auto& realm = this->realm();
// 1. Let format, key, wrappingKey and algorithm be the format, key, wrappingKey and wrapAlgorithm parameters passed to the wrapKey() method, respectively.
StringView operation;
auto normalized_algorithm_or_error = [&]() -> WebIDL::ExceptionOr<NormalizedAlgorithmAndParameter> {
// 2. Let normalizedAlgorithm be the result of normalizing an algorithm, with alg set to algorithm and op set to "wrapKey".
auto normalized_algorithm_wrap_key_or_error = normalize_an_algorithm(realm, algorithm, "wrapKey"_string);
// 3. If an error occurred, let normalizedAlgorithm be the result of normalizing an algorithm, with alg set to algorithm and op set to "encrypt".
// 4. If an error occurred, return a Promise rejected with normalizedAlgorithm.
if (normalized_algorithm_wrap_key_or_error.is_error()) {
auto normalized_algorithm_encrypt_or_error = normalize_an_algorithm(realm, algorithm, "encrypt"_string);
if (normalized_algorithm_encrypt_or_error.is_error())
return normalized_algorithm_encrypt_or_error.release_error();
operation = "encrypt"sv;
return normalized_algorithm_encrypt_or_error.release_value();
} else {
operation = "wrapKey"sv;
return normalized_algorithm_wrap_key_or_error.release_value();
}
}();
if (normalized_algorithm_or_error.is_error())
return WebIDL::create_rejected_promise_from_exception(realm, normalized_algorithm_or_error.release_error());
auto normalized_algorithm = normalized_algorithm_or_error.release_value();
// 5. Let promise be a new Promise.
auto promise = WebIDL::create_promise(realm);
// 6. Return promise and perform the remaining steps in parallel.
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, normalized_algorithm = move(normalized_algorithm), promise, wrapping_key = move(wrapping_key), key = move(key), format, operation]() mutable -> void {
HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// 7. If the following steps or referenced procedures say to throw an error, reject promise with the returned error and then terminate the algorithm.
// 8. If the name member of normalizedAlgorithm is not equal to the name attribute
// of the [[algorithm]] internal slot of wrappingKey then throw an InvalidAccessError.
if (normalized_algorithm.parameter->name != wrapping_key->algorithm_name()) {
WebIDL::reject_promise(realm, promise, WebIDL::InvalidAccessError::create(realm, "Algorithm mismatch"_string));
return;
}
// 9. If the [[usages]] internal slot of wrappingKey does not contain an entry that is "wrapKey", then throw an InvalidAccessError.
if (!wrapping_key->internal_usages().contains_slow(Bindings::KeyUsage::Wrapkey)) {
WebIDL::reject_promise(realm, promise, WebIDL::InvalidAccessError::create(realm, "Key does not support wrapping keys"_string));
return;
}
// 10. If the algorithm identified by the [[algorithm]] internal slot of key does not support the export key operation, then throw a NotSupportedError.
// Note: Handled by the base AlgorithmMethods implementation
// 11. If the [[extractable]] internal slot of key is false, then throw an InvalidAccessError.
if (!key->extractable()) {
WebIDL::reject_promise(realm, promise, WebIDL::InvalidAccessError::create(realm, "Key is not extractable"_string));
return;
}
// 12. Let key be the result of performing the export key operation specified the [[algorithm]] internal slot of key using key and format.
// NOTE: The spec does not mention we need to normalize this, but it's the only way we have to get to export_key.
auto& key_algorithm = verify_cast<KeyAlgorithm>(*key->algorithm());
auto normalized_key_algorithm = normalize_an_algorithm(realm, key_algorithm.name(), "exportKey"_string);
if (normalized_key_algorithm.is_error()) {
WebIDL::reject_promise(realm, promise, Bindings::exception_to_throw_completion(realm.vm(), normalized_key_algorithm.release_error()).release_value().value());
return;
}
auto key_data_or_error = normalized_key_algorithm.release_value().methods->export_key(format, key);
if (key_data_or_error.is_error()) {
WebIDL::reject_promise(realm, promise, Bindings::exception_to_throw_completion(realm.vm(), key_data_or_error.release_error()).release_value().value());
return;
}
auto key_data = key_data_or_error.release_value();
ByteBuffer bytes;
// 13. If format is equal to the strings "raw", "pkcs8", or "spki":
if (format == Bindings::KeyFormat::Raw || format == Bindings::KeyFormat::Pkcs8 || format == Bindings::KeyFormat::Spki) {
// Set bytes be set to key.
bytes = verify_cast<JS::ArrayBuffer>(*key_data).buffer();
}
// If format is equal to the string "jwk":
else if (format == Bindings::KeyFormat::Jwk) {
// 1. Convert key to an ECMAScript Object, as specified in [WEBIDL],
// performing the conversion in the context of a new global object.
// 2. Let json be the result of representing key as a UTF-16 string conforming to the JSON grammar;
// for example, by executing the JSON.stringify algorithm specified in [ECMA-262] in the context of a new global object.
auto maybe_json = JS::JSONObject::stringify_impl(realm.vm(), key_data, JS::Value {}, JS::Value {});
if (maybe_json.is_error()) {
WebIDL::reject_promise(realm, promise, maybe_json.release_error().release_value().value());
return;
}
// 3. Let bytes be the result of UTF-8 encoding json.
bytes = maybe_json.release_value()->to_byte_buffer();
} else {
VERIFY_NOT_REACHED();
}
JS::Value result;
// 14. If normalizedAlgorithm supports the wrap key operation:
if (operation == "wrapKey") {
// Let result be the result of performing the wrap key operation specified by normalizedAlgorithm
// using algorithm, wrappingKey as key and bytes as plaintext.
auto result_or_error = normalized_algorithm.methods->wrap_key(*normalized_algorithm.parameter, wrapping_key, bytes);
if (result_or_error.is_error()) {
WebIDL::reject_promise(realm, promise, Bindings::exception_to_throw_completion(realm.vm(), result_or_error.release_error()).release_value().value());
return;
}
result = result_or_error.release_value();
}
// Otherwise, if normalizedAlgorithm supports the encrypt operation:
else if (operation == "encrypt") {
// Let result be the result of performing the encrypt operation specified by normalizedAlgorithm
// using algorithm, wrappingKey as key and bytes as plaintext.
auto result_or_error = normalized_algorithm.methods->encrypt(*normalized_algorithm.parameter, wrapping_key, bytes);
if (result_or_error.is_error()) {
WebIDL::reject_promise(realm, promise, Bindings::exception_to_throw_completion(realm.vm(), result_or_error.release_error()).release_value().value());
return;
}
result = result_or_error.release_value();
}
// Otherwise:
else {
// throw a NotSupportedError.
WebIDL::reject_promise(realm, promise, WebIDL::NotSupportedError::create(realm, "Algorithm does not support wrapping"_string));
return;
}
// 15. Resolve promise with result.
WebIDL::resolve_promise(realm, promise, result);
}));
return promise;
}
// https://w3c.github.io/webcrypto/#SubtleCrypto-method-unwrapKey
JS::ThrowCompletionOr<GC::Ref<WebIDL::Promise>> SubtleCrypto::unwrap_key(Bindings::KeyFormat format, KeyDataType wrapped_key, GC::Ref<CryptoKey> unwrapping_key, AlgorithmIdentifier algorithm, AlgorithmIdentifier unwrapped_key_algorithm, bool extractable, Vector<Bindings::KeyUsage> key_usages)
{
auto& realm = this->realm();
// 1. Let format, unwrappingKey, algorithm, unwrappedKeyAlgorithm, extractable and usages, be the format, unwrappingKey, unwrapAlgorithm,
// unwrappedKeyAlgorithm, extractable and keyUsages parameters passed to the unwrapKey() method, respectively.
// 2. Let wrappedKey be the result of getting a copy of the bytes held by the wrappedKey parameter passed to the unwrapKey() method.
auto real_wrapped_key = MUST(WebIDL::get_buffer_source_copy(*wrapped_key.get<GC::Root<WebIDL::BufferSource>>()->raw_object()));
StringView operation;
auto normalized_algorithm_or_error = [&]() -> WebIDL::ExceptionOr<NormalizedAlgorithmAndParameter> {
// 3. Let normalizedAlgorithm be the result of normalizing an algorithm, with alg set to algorithm and op set to "unwrapKey".
auto normalized_algorithm_unwrap_key_or_error = normalize_an_algorithm(realm, algorithm, "unwrapKey"_string);
// 4. If an error occurred, let normalizedAlgorithm be the result of normalizing an algorithm, with alg set to algorithm and op set to "decrypt".
// 5. If an error occurred, return a Promise rejected with normalizedAlgorithm.
if (normalized_algorithm_unwrap_key_or_error.is_error()) {
auto normalized_algorithm_decrypt_or_error = normalize_an_algorithm(realm, algorithm, "decrypt"_string);
if (normalized_algorithm_decrypt_or_error.is_error())
return normalized_algorithm_decrypt_or_error.release_error();
operation = "decrypt"sv;
return normalized_algorithm_decrypt_or_error.release_value();
} else {
operation = "unwrapKey"sv;
return normalized_algorithm_unwrap_key_or_error.release_value();
}
}();
if (normalized_algorithm_or_error.is_error())
return WebIDL::create_rejected_promise_from_exception(realm, normalized_algorithm_or_error.release_error());
auto normalized_algorithm = normalized_algorithm_or_error.release_value();
// 6. Let normalizedKeyAlgorithm be the result of normalizing an algorithm, with alg set to unwrappedKeyAlgorithm and op set to "importKey".
auto normalized_key_algorithm_or_error = normalize_an_algorithm(realm, unwrapped_key_algorithm, "importKey"_string);
if (normalized_key_algorithm_or_error.is_error()) {
// 7. If an error occurred, return a Promise rejected with normalizedKeyAlgorithm.
return WebIDL::create_rejected_promise_from_exception(realm, normalized_key_algorithm_or_error.release_error());
}
auto normalized_key_algorithm = normalized_key_algorithm_or_error.release_value();
// 8. Let promise be a new Promise.
auto promise = WebIDL::create_promise(realm);
// 9. Return promise and perform the remaining steps in parallel.
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, normalized_algorithm = move(normalized_algorithm), promise, unwrapping_key = unwrapping_key, real_wrapped_key = move(real_wrapped_key), operation, format, extractable, key_usages = move(key_usages), normalized_key_algorithm = move(normalized_key_algorithm)]() mutable -> void {
HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// 10. If the following steps or referenced procedures say to throw an error, reject promise with the returned error and then terminate the algorithm.
// 11. If the name member of normalizedAlgorithm is not equal to the name attribute of the [[algorithm]] internal slot
// of unwrappingKey then throw an InvalidAccessError.
if (normalized_algorithm.parameter->name != unwrapping_key->algorithm_name()) {
WebIDL::reject_promise(realm, promise, WebIDL::InvalidAccessError::create(realm, "Algorithm mismatch"_string));
return;
}
// 12. If the [[usages]] internal slot of unwrappingKey does not contain an entry that is "unwrapKey", then throw an InvalidAccessError.
if (!unwrapping_key->internal_usages().contains_slow(Bindings::KeyUsage::Unwrapkey)) {
WebIDL::reject_promise(realm, promise, WebIDL::InvalidAccessError::create(realm, "Key does not support unwrapping keys"_string));
return;
}
auto key_or_error = [&]() -> WebIDL::ExceptionOr<GC::Ref<JS::ArrayBuffer>> {
// 13. If normalizedAlgorithm supports an unwrap key operation:
if (operation == "unwrapKey") {
// Let key be the result of performing the unwrap key operation specified by normalizedAlgorithm
// using algorithm, unwrappingKey as key and wrappedKey as ciphertext.
return normalized_algorithm.methods->unwrap_key(*normalized_algorithm.parameter, unwrapping_key, real_wrapped_key);
}
// Otherwise, if normalizedAlgorithm supports a decrypt operation:
else if (operation == "decrypt") {
// Let key be the result of performing the decrypt operation specified by normalizedAlgorithm
// using algorithm, unwrappingKey as key and wrappedKey as ciphertext.
return normalized_algorithm.methods->decrypt(*normalized_algorithm.parameter, unwrapping_key, real_wrapped_key);
}
// Otherwise:
else {
// throw a NotSupportedError.
return WebIDL::NotSupportedError::create(realm, "Algorithm does not support wrapping"_string);
}
}();
if (key_or_error.is_error()) {
WebIDL::reject_promise(realm, promise, Bindings::exception_to_throw_completion(realm.vm(), key_or_error.release_error()).release_value().value());
return;
}
auto key = key_or_error.release_value();
Variant<ByteBuffer, Bindings::JsonWebKey, Empty> bytes;
// 14. If format is equal to the strings "raw", "pkcs8", or "spki":
if (format == Bindings::KeyFormat::Raw || format == Bindings::KeyFormat::Pkcs8 || format == Bindings::KeyFormat::Spki) {
// Set bytes be set to key.
bytes = key->buffer();
}
// If format is equal to the string "jwk":
else if (format == Bindings::KeyFormat::Jwk) {
// Let bytes be the result of executing the parse a JWK algorithm, with key as the data to be parsed.
auto maybe_parsed = Bindings::JsonWebKey::parse(realm, key->buffer());
if (maybe_parsed.is_error()) {
WebIDL::reject_promise(realm, promise, maybe_parsed.release_error().release_value().value());
return;
}
bytes = maybe_parsed.release_value();
} else {
VERIFY_NOT_REACHED();
}
// 15. Let result be the result of performing the import key operation specified by normalizedKeyAlgorithm
// using unwrappedKeyAlgorithm as algorithm, format, usages and extractable and with bytes as keyData.
auto result_or_error = normalized_key_algorithm.methods->import_key(*normalized_key_algorithm.parameter, format, bytes.downcast<CryptoKey::InternalKeyData>(), extractable, key_usages);
if (result_or_error.is_error()) {
WebIDL::reject_promise(realm, promise, Bindings::exception_to_throw_completion(realm.vm(), result_or_error.release_error()).release_value().value());
return;
}
auto result = result_or_error.release_value();
// 16. If the [[type]] internal slot of result is "secret" or "private" and usages is empty, then throw a SyntaxError.
if ((result->type() == Bindings::KeyType::Secret || result->type() == Bindings::KeyType::Private) && key_usages.is_empty()) {
WebIDL::reject_promise(realm, promise, WebIDL::SyntaxError::create(realm, "Usages must not be empty"_string));
return;
}
// 17. Set the [[extractable]] internal slot of result to extractable.
result->set_extractable(extractable);
// 18. Set the [[usages]] internal slot of result to the normalized value of usages.
normalize_key_usages(key_usages);
result->set_usages(key_usages);
// 19. Resolve promise with result.
WebIDL::resolve_promise(realm, promise, result);
}));
return promise;
}
SupportedAlgorithmsMap& supported_algorithms_internal()
{
static SupportedAlgorithmsMap s_supported_algorithms;