diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp index d2eb79a7bc..a8579c76cc 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp @@ -38,6 +38,7 @@ static bool is_platform_object(Type const& type) "CanvasGradient"sv, "CanvasPattern"sv, "CanvasRenderingContext2D"sv, + "CryptoKey"sv, "Document"sv, "DocumentType"sv, "EventTarget"sv, diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/Crypto/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/Crypto/BUILD.gn index 72ad8f540e..9b5a8694e3 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/Crypto/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/Crypto/BUILD.gn @@ -6,6 +6,7 @@ source_set("Crypto") { "Crypto.h", "CryptoAlgorithms.cpp", "CryptoAlgorithms.h", + "CryptoBindings.cpp", "CryptoBindings.h", "CryptoKey.cpp", "CryptoKey.h", diff --git a/Tests/LibWeb/Text/expected/Crypto/SubtleCrypto-exportKey.txt b/Tests/LibWeb/Text/expected/Crypto/SubtleCrypto-exportKey.txt new file mode 100644 index 0000000000..85d39ebb2a --- /dev/null +++ b/Tests/LibWeb/Text/expected/Crypto/SubtleCrypto-exportKey.txt @@ -0,0 +1,13 @@ +exportKey with RSA-OAEP algorithm +exportedPublicKey kwt: RSA +exportedPublicKey alg: RSA-OAEP-256 +exportedPublicKey exponent: AQAB +exportedPublicKey key_ops: encrypt,wrapKey +exportedPublicKey ext: true +exportedPrivateKey kwt: RSA +exportedPrivateKey alg: RSA-OAEP-256 +exportedPrivateKey exponent: AQAB +exportedPrivateKey key_ops: decrypt,unwrapKey +exportedPrivateKey ext: true +FIXME: exportedPublicKey2: 4649584d45 +exportKey spki private key: InvalidAccessError Key is not public diff --git a/Tests/LibWeb/Text/input/Crypto/SubtleCrypto-exportKey.html b/Tests/LibWeb/Text/input/Crypto/SubtleCrypto-exportKey.html new file mode 100644 index 0000000000..82d1562731 --- /dev/null +++ b/Tests/LibWeb/Text/input/Crypto/SubtleCrypto-exportKey.html @@ -0,0 +1,52 @@ + + diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 96850dc0dc..f510fa752e 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -26,6 +26,7 @@ set(SOURCES Clipboard/Clipboard.cpp Crypto/Crypto.cpp Crypto/CryptoAlgorithms.cpp + Crypto/CryptoBindings.cpp Crypto/KeyAlgorithms.cpp Crypto/CryptoKey.cpp Crypto/SubtleCrypto.cpp diff --git a/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp b/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp index 5152b6f8fc..a66507a78c 100644 --- a/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp +++ b/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -57,6 +58,34 @@ static ::Crypto::UnsignedBigInteger big_integer_from_api_big_integer(JS::GCPtr base64_url_uint_encode(::Crypto::UnsignedBigInteger integer) +{ + static_assert(AK::HostIsLittleEndian, "This code assumes little-endian"); + + // The representation of a positive or zero integer value as the + // base64url encoding of the value's unsigned big-endian + // representation as an octet sequence. The octet sequence MUST + // utilize the minimum number of octets needed to represent the + // value. Zero is represented as BASE64URL(single zero-valued + // octet), which is "AA". + + auto bytes = TRY(ByteBuffer::create_uninitialized(integer.trimmed_byte_length())); + + bool const remove_leading_zeroes = true; + auto data_size = integer.export_data(bytes.span(), remove_leading_zeroes); + + auto data_slice = bytes.bytes().slice(bytes.size() - data_size, data_size); + + // We need to encode the integer's big endian representation as a base64 string + Vector byte_swapped_data; + byte_swapped_data.ensure_capacity(data_size); + for (size_t i = 0; i < data_size; ++i) + byte_swapped_data.append(data_slice[data_size - i - 1]); + + return encode_base64(byte_swapped_data); +} + AlgorithmParams::~AlgorithmParams() = default; JS::ThrowCompletionOr> AlgorithmParams::from_value(JS::VM& vm, JS::Value value) @@ -227,6 +256,129 @@ WebIDL::ExceptionOr, JS::NonnullGCPtr, JS::NonnullGCPtr> { CryptoKeyPair::create(m_realm, public_key, private_key) }; } +// https://w3c.github.io/webcrypto/#rsa-oaep-operations +WebIDL::ExceptionOr> RSAOAEP::export_key(Bindings::KeyFormat format, JS::NonnullGCPtr key) +{ + auto& realm = m_realm; + auto& vm = realm.vm(); + + // 1. Let key be the key to be exported. + + // 2. If the underlying cryptographic key material represented by the [[handle]] internal slot of key cannot be accessed, then throw an OperationError. + // Note: In our impl this is always accessible + auto const& handle = key->handle(); + + JS::GCPtr result = nullptr; + + // 3. If format is "spki" + if (format == Bindings::KeyFormat::Spki) { + // 1. If the [[type]] internal slot of key is not "public", then throw an InvalidAccessError. + if (key->type() != Bindings::KeyType::Public) + return WebIDL::InvalidAccessError::create(realm, "Key is not public"_fly_string); + + // FIXME: 2. Let data be an instance of the subjectPublicKeyInfo ASN.1 structure defined in [RFC5280] with the following properties: + // - Set the algorithm field to an AlgorithmIdentifier ASN.1 type with the following properties: + // - Set the algorithm field to the OID rsaEncryption defined in [RFC3447]. + // - Set the params field to the ASN.1 type NULL. + // - Set the subjectPublicKey field to the result of DER-encoding an RSAPublicKey ASN.1 type, as defined in [RFC3447], Appendix A.1.1, + // that represents the RSA public key represented by the [[handle]] internal slot of key + + // FIXME: 3. Let result be the result of creating an ArrayBuffer containing data. + result = JS::ArrayBuffer::create(realm, TRY_OR_THROW_OOM(vm, ByteBuffer::copy(("FIXME"sv).bytes()))); + } + + // FIXME: If format is "pkcs8" + + // If format is "jwk" + else if (format == Bindings::KeyFormat::Jwk) { + // 1. Let jwk be a new JsonWebKey dictionary. + Bindings::JsonWebKey jwk = {}; + + // 2. Set the kty attribute of jwk to the string "RSA". + jwk.kty = "RSA"_string; + + // 4. Let hash be the name attribute of the hash attribute of the [[algorithm]] internal slot of key. + auto hash = TRY(verify_cast(*key->algorithm()).hash().visit([](String const& name) -> JS::ThrowCompletionOr { return name; }, [&](JS::Handle const& obj) -> JS::ThrowCompletionOr { + auto name_property = TRY(obj->get("name")); + return name_property.to_string(realm.vm()); })); + + // 4. If hash is "SHA-1": + // - Set the alg attribute of jwk to the string "RSA-OAEP". + if (hash == "SHA-1"sv) { + jwk.alg = "RSA-OAEP"_string; + } + // If hash is "SHA-256": + // - Set the alg attribute of jwk to the string "RSA-OAEP-256". + else if (hash == "SHA-256"sv) { + jwk.alg = "RSA-OAEP-256"_string; + } + // If hash is "SHA-384": + // - Set the alg attribute of jwk to the string "RSA-OAEP-384". + else if (hash == "SHA-384"sv) { + jwk.alg = "RSA-OAEP-384"_string; + } + // If hash is "SHA-512": + // - Set the alg attribute of jwk to the string "RSA-OAEP-512". + else if (hash == "SHA-512"sv) { + jwk.alg = "RSA-OAEP-512"_string; + } else { + // FIXME: Support 'other applicable specifications' + // - Perform any key export steps defined by other applicable specifications, + // passing format and the hash attribute of the [[algorithm]] internal slot of key and obtaining alg. + // - Set the alg attribute of jwk to alg. + return WebIDL::NotSupportedError::create(realm, TRY_OR_THROW_OOM(vm, String::formatted("Unsupported hash algorithm '{}'", hash))); + } + + // 10. Set the attributes n and e of jwk according to the corresponding definitions in JSON Web Algorithms [JWA], Section 6.3.1. + auto maybe_error = handle.visit( + [&](::Crypto::PK::RSAPublicKey<> const& public_key) -> ErrorOr { + jwk.n = TRY(base64_url_uint_encode(public_key.modulus())); + jwk.e = TRY(base64_url_uint_encode(public_key.public_exponent())); + return {}; + }, + [&](::Crypto::PK::RSAPrivateKey<> const& private_key) -> ErrorOr { + jwk.n = TRY(base64_url_uint_encode(private_key.modulus())); + jwk.e = TRY(base64_url_uint_encode(private_key.public_exponent())); + + // 11. If the [[type]] internal slot of key is "private": + // 1. Set the attributes named d, p, q, dp, dq, and qi of jwk according to the corresponding definitions in JSON Web Algorithms [JWA], Section 6.3.2. + jwk.d = TRY(base64_url_uint_encode(private_key.private_exponent())); + // FIXME: Add p, q, dq, qi + + // 12. If the underlying RSA private key represented by the [[handle]] internal slot of key is represented by more than two primes, + // set the attribute named oth of jwk according to the corresponding definition in JSON Web Algorithms [JWA], Section 6.3.2.7 + // FIXME: We don't support more than 2 primes on RSA keys + return {}; + }, + [](auto) -> ErrorOr { + VERIFY_NOT_REACHED(); + }); + // FIXME: clang-format butchers the visit if we do the TRY inline + TRY_OR_THROW_OOM(vm, maybe_error); + + // 13. Set the key_ops attribute of jwk to the usages attribute of key. + jwk.key_ops = Vector {}; + jwk.key_ops->ensure_capacity(key->internal_usages().size()); + for (auto const& usage : key->internal_usages()) { + jwk.key_ops->append(Bindings::idl_enum_to_string(usage)); + } + + // 14. Set the ext attribute of jwk to the [[extractable]] internal slot of key. + jwk.ext = key->extractable(); + + // 15. Let result be the result of converting jwk to an ECMAScript Object, as defined by [WebIDL]. + result = TRY(jwk.to_object(realm)); + } + + // Otherwise throw a NotSupportedError. + else { + return WebIDL::NotSupportedError::create(realm, TRY_OR_THROW_OOM(vm, String::formatted("Exporting to format {} is not supported", Bindings::idl_enum_to_string(format)))); + } + + // 8. Return result + return JS::NonnullGCPtr { *result }; +} + WebIDL::ExceptionOr> PBKDF2::import_key(AlgorithmParams const&, Bindings::KeyFormat format, CryptoKey::InternalKeyData key_data, bool extractable, Vector const& key_usages) { // 1. If format is not "raw", throw a NotSupportedError diff --git a/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.h b/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.h index a5f801e455..78075bdbce 100644 --- a/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.h +++ b/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.h @@ -106,6 +106,11 @@ public: return WebIDL::NotSupportedError::create(m_realm, "generateKey is not supported"_fly_string); } + virtual WebIDL::ExceptionOr> export_key(Bindings::KeyFormat, JS::NonnullGCPtr) + { + return WebIDL::NotSupportedError::create(m_realm, "exportKey is not supported"_fly_string); + } + static NonnullOwnPtr create(JS::Realm& realm) { return adopt_own(*new AlgorithmMethods(realm)); } protected: @@ -120,6 +125,7 @@ protected: class RSAOAEP : public AlgorithmMethods { public: virtual WebIDL::ExceptionOr, JS::NonnullGCPtr>> generate_key(AlgorithmParams const&, bool, Vector const&) override; + virtual WebIDL::ExceptionOr> export_key(Bindings::KeyFormat, JS::NonnullGCPtr) override; static NonnullOwnPtr create(JS::Realm& realm) { return adopt_own(*new RSAOAEP(realm)); } @@ -156,4 +162,6 @@ private: } }; +ErrorOr base64_url_uint_encode(::Crypto::UnsignedBigInteger); + } diff --git a/Userland/Libraries/LibWeb/Crypto/CryptoBindings.cpp b/Userland/Libraries/LibWeb/Crypto/CryptoBindings.cpp new file mode 100644 index 0000000000..1e911c262d --- /dev/null +++ b/Userland/Libraries/LibWeb/Crypto/CryptoBindings.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace Web::Bindings { + +JS::ThrowCompletionOr> JsonWebKey::to_object(JS::Realm& realm) +{ + auto& vm = realm.vm(); + auto object = JS::Object::create(realm, realm.intrinsics().object_prototype()); + + if (kty.has_value()) + TRY(object->create_data_property("kty", JS::PrimitiveString::create(vm, kty.value()))); + + if (use.has_value()) + TRY(object->create_data_property("use", JS::PrimitiveString::create(vm, use.value()))); + + if (key_ops.has_value()) { + auto key_ops_array = JS::Array::create_from(realm, key_ops.value().span(), [&](auto& key_usage) -> JS::Value { + return JS::PrimitiveString::create(realm.vm(), key_usage); + }); + TRY(object->create_data_property("key_ops", move(key_ops_array))); + } + + if (alg.has_value()) + TRY(object->create_data_property("alg", JS::PrimitiveString::create(vm, alg.value()))); + + if (ext.has_value()) + TRY(object->create_data_property("ext", JS::Value(ext.value()))); + + if (crv.has_value()) + TRY(object->create_data_property("crv", JS::PrimitiveString::create(vm, crv.value()))); + + if (x.has_value()) + TRY(object->create_data_property("x", JS::PrimitiveString::create(vm, x.value()))); + + if (y.has_value()) + TRY(object->create_data_property("y", JS::PrimitiveString::create(vm, y.value()))); + + if (d.has_value()) + TRY(object->create_data_property("d", JS::PrimitiveString::create(vm, d.value()))); + + if (n.has_value()) + TRY(object->create_data_property("n", JS::PrimitiveString::create(vm, n.value()))); + + if (e.has_value()) + TRY(object->create_data_property("e", JS::PrimitiveString::create(vm, e.value()))); + + if (p.has_value()) + TRY(object->create_data_property("p", JS::PrimitiveString::create(vm, p.value()))); + + if (q.has_value()) + TRY(object->create_data_property("q", JS::PrimitiveString::create(vm, q.value()))); + + if (dp.has_value()) + TRY(object->create_data_property("dp", JS::PrimitiveString::create(vm, dp.value()))); + + if (dq.has_value()) + TRY(object->create_data_property("dq", JS::PrimitiveString::create(vm, dq.value()))); + + if (qi.has_value()) + TRY(object->create_data_property("qi", JS::PrimitiveString::create(vm, qi.value()))); + + if (oth.has_value()) { + TODO(); + } + + if (k.has_value()) + TRY(object->create_data_property("k", JS::PrimitiveString::create(vm, k.value()))); + + return object; +} + +} diff --git a/Userland/Libraries/LibWeb/Crypto/CryptoBindings.h b/Userland/Libraries/LibWeb/Crypto/CryptoBindings.h index 437f308714..3d8d506dd6 100644 --- a/Userland/Libraries/LibWeb/Crypto/CryptoBindings.h +++ b/Userland/Libraries/LibWeb/Crypto/CryptoBindings.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2023, stelar7 + * Copyright (c) 2024, Andrew Kaster * * SPDX-License-Identifier: BSD-2-Clause */ @@ -9,8 +10,7 @@ #include #include #include -#include -#include +#include // FIXME: Generate these from IDL namespace Web::Bindings { @@ -42,6 +42,8 @@ struct JsonWebKey { Optional qi; Optional> oth; Optional k; + + JS::ThrowCompletionOr> to_object(JS::Realm&); }; } diff --git a/Userland/Libraries/LibWeb/Crypto/CryptoKey.h b/Userland/Libraries/LibWeb/Crypto/CryptoKey.h index 66d76e0c17..48d1185a90 100644 --- a/Userland/Libraries/LibWeb/Crypto/CryptoKey.h +++ b/Userland/Libraries/LibWeb/Crypto/CryptoKey.h @@ -39,6 +39,8 @@ public: void set_algorithm(JS::NonnullGCPtr algorithm) { m_algorithm = move(algorithm); } void set_usages(Vector); + InternalKeyData const& handle() const { return m_key_data; } + private: CryptoKey(JS::Realm&, InternalKeyData); virtual void initialize(JS::Realm&) override; @@ -50,7 +52,7 @@ private: JS::NonnullGCPtr m_usages; Vector m_key_usages; - InternalKeyData m_key_data; + InternalKeyData m_key_data; // [[handle]] }; // https://w3c.github.io/webcrypto/#ref-for-dfn-CryptoKeyPair-2 diff --git a/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.cpp b/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.cpp index 00c3b90e3e..32f26d67ed 100644 --- a/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.cpp +++ b/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -289,6 +290,52 @@ JS::ThrowCompletionOr> SubtleCrypto::import_key(Bi return verify_cast(*promise->promise()); } +// https://w3c.github.io/webcrypto/#dfn-SubtleCrypto-method-exportKey +JS::ThrowCompletionOr> SubtleCrypto::export_key(Bindings::KeyFormat format, JS::NonnullGCPtr key) +{ + auto& realm = this->realm(); + // 1. Let format and key be the format and key parameters passed to the exportKey() method, respectively. + + // 2. Let promise be a new Promise. + auto promise = WebIDL::create_promise(realm); + + // 3. Return promise and perform the remaining steps in parallel. + Platform::EventLoopPlugin::the().deferred_invoke([&realm, key, this, promise, format]() -> void { + HTML::TemporaryExecutionContext context(Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); + // 4. If the following steps or referenced procedures say to throw an error, reject promise with the returned error and then terminate the algorithm. + + // 5. If the name member of the [[algorithm]] internal slot of key does not identify a registered algorithm that supports the export key operation, + // then throw a NotSupportedError. + // Note: Handled by the base AlgorithmMethods implementation + auto& algorithm = verify_cast(*key->algorithm()); + // FIXME: Stash the AlgorithmMethods on the KeyAlgorithm + auto normalized_algorithm_or_error = normalize_an_algorithm(algorithm.name(), "exportKey"_string); + if (normalized_algorithm_or_error.is_error()) { + WebIDL::reject_promise(realm, promise, Bindings::dom_exception_to_throw_completion(realm.vm(), normalized_algorithm_or_error.release_error()).release_value().value()); + return; + } + auto normalized_algorithm = normalized_algorithm_or_error.release_value(); + + // 6. 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"_fly_string)); + return; + } + + // 7. Let result be the result of performing the export key operation specified by the [[algorithm]] internal slot of key using key and format. + auto result_or_error = normalized_algorithm.methods->export_key(format, key); + if (result_or_error.is_error()) { + WebIDL::reject_promise(realm, promise, Bindings::dom_exception_to_throw_completion(realm.vm(), result_or_error.release_error()).release_value().value()); + return; + } + + // 8. Resolve promise with result. + WebIDL::resolve_promise(realm, promise, result_or_error.release_value()); + }); + + return verify_cast(*promise->promise()); +} + SubtleCrypto::SupportedAlgorithmsMap& SubtleCrypto::supported_algorithms_internal() { static SubtleCrypto::SupportedAlgorithmsMap s_supported_algorithms; @@ -339,7 +386,8 @@ SubtleCrypto::SupportedAlgorithmsMap SubtleCrypto::supported_algorithms() // https://w3c.github.io/webcrypto/#rsa-oaep define_an_algorithm("generateKey"_string, "RSA-OAEP"_string); - // FIXME: encrypt, decrypt, importKey, exportKey + define_an_algorithm("exportKey"_string, "RSA-OAEP"_string); + // FIXME: encrypt, decrypt, importKey return internal_object; } diff --git a/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.h b/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.h index 1dd973b11c..cd1ed265b1 100644 --- a/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.h +++ b/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.h @@ -14,7 +14,6 @@ #include #include #include -#include #include namespace Web::Crypto { @@ -39,6 +38,7 @@ public: JS::ThrowCompletionOr> generate_key(AlgorithmIdentifier algorithm, bool extractable, Vector key_usages); JS::ThrowCompletionOr> import_key(Bindings::KeyFormat format, KeyDataType key_data, AlgorithmIdentifier algorithm, bool extractable, Vector key_usages); + JS::ThrowCompletionOr> export_key(Bindings::KeyFormat format, JS::NonnullGCPtr key); private: explicit SubtleCrypto(JS::Realm&); diff --git a/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.idl b/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.idl index 931e0ed44f..27643a42b2 100644 --- a/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.idl +++ b/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.idl @@ -57,7 +57,7 @@ interface SubtleCrypto { // FIXME: Promise deriveBits(AlgorithmIdentifier algorithm, CryptoKey baseKey, unsigned long length); Promise importKey(KeyFormat format, (BufferSource or JsonWebKey) keyData, AlgorithmIdentifier algorithm, boolean extractable, sequence keyUsages); - // FIXME: Promise exportKey(KeyFormat format, CryptoKey key); + Promise exportKey(KeyFormat format, CryptoKey key); // FIXME: Promise wrapKey(KeyFormat format, CryptoKey key, CryptoKey wrappingKey, AlgorithmIdentifier wrapAlgorithm); // FIXME: Promise unwrapKey(KeyFormat format, BufferSource wrappedKey, CryptoKey unwrappingKey, AlgorithmIdentifier unwrapAlgorithm, AlgorithmIdentifier unwrappedKeyAlgorithm, boolean extractable, sequence keyUsages);