diff --git a/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp b/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp index 70f523a2dc4..05910b6849a 100644 --- a/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp +++ b/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp @@ -302,9 +302,10 @@ static WebIDL::ExceptionOr validate_jwk_key_ops(JS::Realm& realm, Bindings return {}; } -static WebIDL::ExceptionOr generate_aes_key(JS::VM& vm, u16 const size_in_bits) +static WebIDL::ExceptionOr generate_random_key(JS::VM& vm, u16 const size_in_bits) { auto key_buffer = TRY_OR_THROW_OOM(vm, ByteBuffer::create_uninitialized(size_in_bits / 8)); + // FIXME: Use a cryptographically secure random generator fill_with_random(key_buffer); return key_buffer; } @@ -606,6 +607,48 @@ JS::ThrowCompletionOr> EcdhKeyDerivePrams::from_v return adopt_own(*new EcdhKeyDerivePrams { name, key }); } +HmacImportParams::~HmacImportParams() = default; + +JS::ThrowCompletionOr> HmacImportParams::from_value(JS::VM& vm, JS::Value value) +{ + auto& object = value.as_object(); + + auto name_value = TRY(object.get("name")); + auto name = TRY(name_value.to_string(vm)); + + auto hash_value = TRY(object.get("hash")); + auto hash = TRY(hash_algorithm_identifier_from_value(vm, hash_value)); + + auto maybe_length = Optional {}; + if (MUST(object.has_property("length"))) { + auto length_value = TRY(object.get("length")); + maybe_length = TRY(length_value.to_u32(vm)); + } + + return adopt_own(*new HmacImportParams { name, hash, maybe_length }); +} + +HmacKeyGenParams::~HmacKeyGenParams() = default; + +JS::ThrowCompletionOr> HmacKeyGenParams::from_value(JS::VM& vm, JS::Value value) +{ + auto& object = value.as_object(); + + auto name_value = TRY(object.get("name")); + auto name = TRY(name_value.to_string(vm)); + + auto hash_value = TRY(object.get("hash")); + auto hash = TRY(hash_algorithm_identifier_from_value(vm, hash_value)); + + auto maybe_length = Optional {}; + if (MUST(object.has_property("length"))) { + auto length_value = TRY(object.get("length")); + maybe_length = TRY(length_value.to_u32(vm)); + } + + return adopt_own(*new HmacKeyGenParams { name, hash, maybe_length }); +} + // https://w3c.github.io/webcrypto/#rsa-oaep-operations WebIDL::ExceptionOr> RSAOAEP::encrypt(AlgorithmParams const& params, JS::NonnullGCPtr key, ByteBuffer const& plaintext) { @@ -1395,7 +1438,7 @@ WebIDL::ExceptionOr, JS::NonnullGCPtrvm(), bits)); + auto key_buffer = TRY(generate_random_key(m_realm->vm(), bits)); // 4. If the key generation step fails, then throw an OperationError. // Note: Cannot happen in our implementation; and if we OOM, then allocating the Exception is probably going to crash anyway. @@ -1721,7 +1764,7 @@ WebIDL::ExceptionOr, JS::NonnullGCPtrvm(), bits)); + auto key_buffer = TRY(generate_random_key(m_realm->vm(), bits)); // 5. Let key be a new CryptoKey object representing the generated AES key. auto key = CryptoKey::create(m_realm, CryptoKey::InternalKeyData { key_buffer }); @@ -2144,7 +2187,7 @@ WebIDL::ExceptionOr, JS::NonnullGCPtrvm(), bits)); + auto key_buffer = TRY(generate_random_key(m_realm->vm(), bits)); // 5. Let key be a new CryptoKey object representing the generated AES key. auto key = CryptoKey::create(m_realm, CryptoKey::InternalKeyData { key_buffer }); @@ -2815,8 +2858,7 @@ WebIDL::ExceptionOr> X25519::derive_bits(Algor // Otherwise: Return an octet string containing the first length bits of secret. auto slice = TRY_OR_THROW_OOM(realm.vm(), secret.slice(0, length / 8)); - auto result = TRY_OR_THROW_OOM(realm.vm(), ByteBuffer::copy(slice)); - return JS::ArrayBuffer::create(realm, move(result)); + return JS::ArrayBuffer::create(realm, move(slice)); } WebIDL::ExceptionOr, JS::NonnullGCPtr>> X25519::generate_key([[maybe_unused]] AlgorithmParams const& params, bool extractable, Vector const& key_usages) @@ -3208,7 +3250,8 @@ WebIDL::ExceptionOr> X25519::export_key(Bindings::K // 6. Set the key_ops attribute of jwk to the usages attribute of key. auto key_ops = Vector {}; auto key_usages = verify_cast(key->usages()); - for (auto i = 0; i < 10; ++i) { + auto key_usages_length = MUST(MUST(key_usages->get(vm.names.length)).to_length(vm)); + for (auto i = 0u; i < key_usages_length; ++i) { auto usage = key_usages->get(i); if (!usage.has_value()) break; @@ -3248,4 +3291,427 @@ WebIDL::ExceptionOr> X25519::export_key(Bindings::K return JS::NonnullGCPtr { *result }; } +static WebIDL::ExceptionOr hmac_calculate_message_digest(JS::Realm& realm, JS::GCPtr hash, ReadonlyBytes key, ReadonlyBytes message) +{ + auto calculate_digest = [&]() -> ByteBuffer { + ::Crypto::Authentication::HMAC hmac(key); + auto digest = hmac.process(message); + return MUST(ByteBuffer::copy(digest.bytes())); + }; + auto hash_name = hash->name(); + if (hash_name.equals_ignoring_ascii_case("SHA-1"sv)) + return calculate_digest.operator()<::Crypto::Hash::SHA1>(); + if (hash_name.equals_ignoring_ascii_case("SHA-256"sv)) + return calculate_digest.operator()<::Crypto::Hash::SHA256>(); + if (hash_name.equals_ignoring_ascii_case("SHA-384"sv)) + return calculate_digest.operator()<::Crypto::Hash::SHA384>(); + if (hash_name.equals_ignoring_ascii_case("SHA-512"sv)) + return calculate_digest.operator()<::Crypto::Hash::SHA512>(); + return WebIDL::NotSupportedError::create(realm, "Invalid algorithm"_string); +} + +static WebIDL::ExceptionOr hmac_hash_block_size(JS::Realm& realm, HashAlgorithmIdentifier hash) +{ + auto hash_name = TRY(hash.name(realm.vm())); + if (hash_name.equals_ignoring_ascii_case("SHA-1"sv)) + return ::Crypto::Hash::SHA1::digest_size(); + if (hash_name.equals_ignoring_ascii_case("SHA-256"sv)) + return ::Crypto::Hash::SHA256::digest_size(); + if (hash_name.equals_ignoring_ascii_case("SHA-384"sv)) + return ::Crypto::Hash::SHA384::digest_size(); + if (hash_name.equals_ignoring_ascii_case("SHA-512"sv)) + return ::Crypto::Hash::SHA512::digest_size(); + return WebIDL::NotSupportedError::create(realm, MUST(String::formatted("Invalid hash function '{}'", hash_name))); +} + +// https://w3c.github.io/webcrypto/#hmac-operations +WebIDL::ExceptionOr> HMAC::sign(AlgorithmParams const&, JS::NonnullGCPtr key, ByteBuffer const& message) +{ + // 1. Let mac be the result of performing the MAC Generation operation described in Section 4 of + // [FIPS-198-1] using the key represented by [[handle]] internal slot of key, the hash + // function identified by the hash attribute of the [[algorithm]] internal slot of key and + // message as the input data text. + auto const& key_data = key->handle().get(); + auto const& algorithm = verify_cast(*key->algorithm()); + auto mac = TRY(hmac_calculate_message_digest(m_realm, algorithm.hash(), key_data.bytes(), message.bytes())); + + // 2. Return the result of creating an ArrayBuffer containing mac. + return JS::ArrayBuffer::create(m_realm, move(mac)); +} + +// https://w3c.github.io/webcrypto/#hmac-operations +WebIDL::ExceptionOr HMAC::verify(AlgorithmParams const&, JS::NonnullGCPtr key, ByteBuffer const& signature, ByteBuffer const& message) +{ + // 1. Let mac be the result of performing the MAC Generation operation described in Section 4 of + // [FIPS-198-1] using the key represented by [[handle]] internal slot of key, the hash + // function identified by the hash attribute of the [[algorithm]] internal slot of key and + // message as the input data text. + auto const& key_data = key->handle().get(); + auto const& algorithm = verify_cast(*key->algorithm()); + auto mac = TRY(hmac_calculate_message_digest(m_realm, algorithm.hash(), key_data.bytes(), message.bytes())); + + // 2. Return true if mac is equal to signature and false otherwise. + return mac == signature; +} + +// https://w3c.github.io/webcrypto/#hmac-operations +WebIDL::ExceptionOr, JS::NonnullGCPtr>> HMAC::generate_key(AlgorithmParams const& params, bool extractable, Vector const& usages) +{ + // 1. If usages contains any entry which is not "sign" or "verify", then throw a SyntaxError. + for (auto const& usage : usages) { + if (usage != Bindings::KeyUsage::Sign && usage != Bindings::KeyUsage::Verify) + return WebIDL::SyntaxError::create(m_realm, MUST(String::formatted("Invalid key usage '{}'", idl_enum_to_string(usage)))); + } + + // 2. If the length member of normalizedAlgorithm is not present: + auto const& normalized_algorithm = static_cast(params); + WebIDL::UnsignedLong length; + if (!normalized_algorithm.length.has_value()) { + // Let length be the block size in bits of the hash function identified by the hash member + // of normalizedAlgorithm. + length = TRY(hmac_hash_block_size(m_realm, normalized_algorithm.hash)); + } + + // Otherwise, if the length member of normalizedAlgorithm is non-zero: + else if (normalized_algorithm.length.value() != 0) { + // Let length be equal to the length member of normalizedAlgorithm. + length = normalized_algorithm.length.value(); + } + + // Otherwise: + else { + // throw an OperationError. + return WebIDL::OperationError::create(m_realm, "Invalid length"_string); + } + + // 3. Generate a key of length length bits. + auto key_data = MUST(generate_random_key(m_realm->vm(), length)); + + // 4. If the key generation step fails, then throw an OperationError. + // NOTE: Currently key generation must succeed + + // 5. Let key be a new CryptoKey object representing the generated key. + auto key = CryptoKey::create(m_realm, move(key_data)); + + // 6. Let algorithm be a new HmacKeyAlgorithm. + auto algorithm = HmacKeyAlgorithm::create(m_realm); + + // 7. Set the name attribute of algorithm to "HMAC". + algorithm->set_name("HMAC"_string); + + // 8. Let hash be a new KeyAlgorithm. + auto hash = KeyAlgorithm::create(m_realm); + + // 9. Set the name attribute of hash to equal the name member of the hash member of normalizedAlgorithm. + hash->set_name(TRY(normalized_algorithm.hash.name(m_realm->vm()))); + + // 10. Set the hash attribute of algorithm to hash. + algorithm->set_hash(hash); + + // 11. Set the [[type]] internal slot of key to "secret". + key->set_type(Bindings::KeyType::Secret); + + // 12. Set the [[algorithm]] internal slot of key to algorithm. + key->set_algorithm(algorithm); + + // 13. Set the [[extractable]] internal slot of key to be extractable. + key->set_extractable(extractable); + + // 14. Set the [[usages]] internal slot of key to be usages. + key->set_usages(usages); + + // 15. Return key. + return Variant, JS::NonnullGCPtr> { key }; +} + +// https://w3c.github.io/webcrypto/#hmac-operations +WebIDL::ExceptionOr> HMAC::import_key(Web::Crypto::AlgorithmParams const& params, Bindings::KeyFormat key_format, CryptoKey::InternalKeyData key_data, bool extractable, Vector const& usages) +{ + auto& vm = m_realm->vm(); + auto const& normalized_algorithm = static_cast(params); + + // 1. Let keyData be the key data to be imported. + // 2. If usages contains an entry which is not "sign" or "verify", then throw a SyntaxError. + for (auto const& usage : usages) { + if (usage != Bindings::KeyUsage::Sign && usage != Bindings::KeyUsage::Verify) + return WebIDL::SyntaxError::create(m_realm, MUST(String::formatted("Invalid key usage '{}'", idl_enum_to_string(usage)))); + } + + // 3. Let hash be a new KeyAlgorithm. + auto hash = KeyAlgorithm::create(m_realm); + + // 4. If format is "raw": + AK::ByteBuffer data; + if (key_format == Bindings::KeyFormat::Raw) { + // 4.1. Let data be the octet string contained in keyData. + data = key_data.get(); + + // 4.2. Set hash to equal the hash member of normalizedAlgorithm. + hash->set_name(TRY(normalized_algorithm.hash.name(vm))); + } + + // If format is "jwk": + else if (key_format == Bindings::KeyFormat::Jwk) { + // 1. If keyData is a JsonWebKey dictionary: + // Let jwk equal keyData. + // Otherwise: + // Throw a DataError. + if (!key_data.has()) + return WebIDL::DataError::create(m_realm, "Data is not a JsonWebKey dictionary"_string); + auto jwk = key_data.get(); + + // 2. If the kty field of jwk is not "oct", then throw a DataError. + if (jwk.kty != "oct"sv) + return WebIDL::DataError::create(m_realm, "Invalid key type"_string); + + // 3. If jwk does not meet the requirements of Section 6.4 of JSON Web Algorithms [JWA], + // then throw a DataError. + // 4. Let data be the octet string obtained by decoding the k field of jwk. + data = TRY(parse_jwk_symmetric_key(m_realm, jwk)); + + // 5. Set the hash to equal the hash member of normalizedAlgorithm. + hash->set_name(TRY(normalized_algorithm.hash.name(vm))); + + // 6. If the name attribute of hash is "SHA-1": + auto hash_name = hash->name(); + if (hash_name.equals_ignoring_ascii_case("SHA-1"sv)) { + // If the alg field of jwk is present and is not "HS1", then throw a DataError. + if (jwk.alg.has_value() && jwk.alg != "HS1"sv) + return WebIDL::DataError::create(m_realm, "Invalid algorithm"_string); + } + + // If the name attribute of hash is "SHA-256": + else if (hash_name.equals_ignoring_ascii_case("SHA-256"sv)) { + // If the alg field of jwk is present and is not "HS256", then throw a DataError. + if (jwk.alg.has_value() && jwk.alg != "HS256"sv) + return WebIDL::DataError::create(m_realm, "Invalid algorithm"_string); + } + + // If the name attribute of hash is "SHA-384": + else if (hash_name.equals_ignoring_ascii_case("SHA-384"sv)) { + // If the alg field of jwk is present and is not "HS384", then throw a DataError. + if (jwk.alg.has_value() && jwk.alg != "HS384"sv) + return WebIDL::DataError::create(m_realm, "Invalid algorithm"_string); + } + + // If the name attribute of hash is "SHA-512": + else if (hash_name.equals_ignoring_ascii_case("SHA-512"sv)) { + // If the alg field of jwk is present and is not "HS512", then throw a DataError. + if (jwk.alg.has_value() && jwk.alg != "HS512"sv) + return WebIDL::DataError::create(m_realm, "Invalid algorithm"_string); + } + + // FIXME: Otherwise, if the name attribute of hash is defined in another applicable specification: + else { + // FIXME: Perform any key import steps defined by other applicable specifications, passing format, + // jwk and hash and obtaining hash. + dbgln("Hash algorithm '{}' not supported", hash_name); + return WebIDL::DataError::create(m_realm, "Invalid algorithm"_string); + } + + // 7. If usages is non-empty and the use field of jwk is present and is not "sign", then + // throw a DataError. + if (!usages.is_empty() && jwk.use.has_value() && jwk.use != "sign"sv) + return WebIDL::DataError::create(m_realm, "Invalid use in JsonWebKey"_string); + + // 8. If the key_ops field of jwk is present, and is invalid according to the requirements + // of JSON Web Key [JWK] or does not contain all of the specified usages values, then + // throw a DataError. + TRY(validate_jwk_key_ops(m_realm, jwk, usages)); + + // 9. If the ext field of jwk is present and has the value false and extractable is true, + // then throw a DataError. + if (jwk.ext.has_value() && !*jwk.ext && extractable) + return WebIDL::DataError::create(m_realm, "Invalid ext field"_string); + } + + // Otherwise: + else { + // throw a NotSupportedError. + return WebIDL::NotSupportedError::create(m_realm, "Invalid key format"_string); + } + + // 5. Let length be equivalent to the length, in octets, of data, multiplied by 8. + auto length = data.size() * 8; + + // 6. If length is zero then throw a DataError. + if (length == 0) + return WebIDL::DataError::create(m_realm, "No data provided"_string); + + // 7. If the length member of normalizedAlgorithm is present: + if (normalized_algorithm.length.has_value()) { + // If the length member of normalizedAlgorithm is greater than length: + auto normalized_algorithm_length = normalized_algorithm.length.value(); + if (normalized_algorithm_length > length) { + // throw a DataError. + return WebIDL::DataError::create(m_realm, "Invalid data size"_string); + } + + // If the length member of normalizedAlgorithm, is less than or equal to length minus eight: + if (normalized_algorithm_length <= length - 8) { + // throw a DataError. + return WebIDL::DataError::create(m_realm, "Invalid data size"_string); + } + + // Otherwise: + // Set length equal to the length member of normalizedAlgorithm. + length = normalized_algorithm_length; + } + + // 8. Let key be a new CryptoKey object representing an HMAC key with the first length bits of data. + auto length_in_bytes = length / 8; + if (data.size() > length_in_bytes) + data = MUST(data.slice(0, length_in_bytes)); + auto key = CryptoKey::create(m_realm, move(data)); + + // 9. Set the [[type]] internal slot of key to "secret". + key->set_type(Bindings::KeyType::Secret); + + // 10. Let algorithm be a new HmacKeyAlgorithm. + auto algorithm = HmacKeyAlgorithm::create(m_realm); + + // 11. Set the name attribute of algorithm to "HMAC". + algorithm->set_name("HMAC"_string); + + // 12. Set the length attribute of algorithm to length. + algorithm->set_length(length); + + // 13. Set the hash attribute of algorithm to hash. + algorithm->set_hash(hash); + + // 14. Set the [[algorithm]] internal slot of key to algorithm. + key->set_algorithm(algorithm); + + // 15. Return key. + return key; +} + +// https://w3c.github.io/webcrypto/#hmac-operations +WebIDL::ExceptionOr> HMAC::export_key(Bindings::KeyFormat format, JS::NonnullGCPtr key) +{ + auto& vm = m_realm->vm(); + + // 1. 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 + + // 2. Let bits be the raw bits of the key represented by [[handle]] internal slot of key. + // 3. Let data be an octet string containing bits. + auto data = key->handle().get(); + + // 4. If format is "raw": + JS::GCPtr result; + if (format == Bindings::KeyFormat::Raw) { + // Let result be the result of creating an ArrayBuffer containing data. + result = JS::ArrayBuffer::create(m_realm, data); + } + + // If format is "jwk": + else if (format == Bindings::KeyFormat::Jwk) { + // Let jwk be a new JsonWebKey dictionary. + Bindings::JsonWebKey jwk {}; + + // Set the kty attribute of jwk to the string "oct". + jwk.kty = "oct"_string; + + // Set the k attribute of jwk to be a string containing data, encoded according to Section + // 6.4 of JSON Web Algorithms [JWA]. + jwk.k = MUST(encode_base64url(data, AK::OmitPadding::Yes)); + + // Let algorithm be the [[algorithm]] internal slot of key. + auto const& algorithm = verify_cast(*key->algorithm()); + + // Let hash be the hash attribute of algorithm. + auto hash = algorithm.hash(); + + // If the name attribute of hash is "SHA-1": + auto hash_name = hash->name(); + if (hash_name.equals_ignoring_ascii_case("SHA-1"sv)) { + // Set the alg attribute of jwk to the string "HS1". + jwk.alg = "HS1"_string; + } + // If the name attribute of hash is "SHA-256": + else if (hash_name.equals_ignoring_ascii_case("SHA-256"sv)) { + // Set the alg attribute of jwk to the string "HS256". + jwk.alg = "HS256"_string; + } + // If the name attribute of hash is "SHA-384": + else if (hash_name.equals_ignoring_ascii_case("SHA-384"sv)) { + // Set the alg attribute of jwk to the string "HS384". + jwk.alg = "HS384"_string; + } + // If the name attribute of hash is "SHA-512": + else if (hash_name.equals_ignoring_ascii_case("SHA-512"sv)) { + // Set the alg attribute of jwk to the string "HS512". + jwk.alg = "HS512"_string; + } + + // FIXME: Otherwise, the name attribute of hash is defined in another applicable + // specification: + else { + // FIXME: Perform any key export steps defined by other applicable specifications, + // passing format and key and obtaining alg. + // FIXME: Set the alg attribute of jwk to alg. + dbgln("Hash algorithm '{}' not supported", hash_name); + return WebIDL::DataError::create(m_realm, "Invalid algorithm"_string); + } + + // Set the key_ops attribute of jwk to equal the usages attribute of key. + auto key_usages = verify_cast(key->usages()); + auto key_usages_length = MUST(MUST(key_usages->get(vm.names.length)).to_length(vm)); + for (auto i = 0u; i < key_usages_length; ++i) { + auto usage = key_usages->get(i); + if (!usage.has_value()) + break; + + auto usage_string = TRY(usage.value().to_string(vm)); + jwk.key_ops->append(usage_string); + } + + // Set the ext attribute of jwk to equal the [[extractable]] internal slot of key. + jwk.ext = key->extractable(); + + // Let result be the result of converting jwk to an ECMAScript Object, as defined by [WebIDL]. + result = TRY(jwk.to_object(m_realm)); + } + + // Otherwise: + else { + // throw a NotSupportedError. + return WebIDL::NotSupportedError::create(m_realm, "Invalid key format"_string); + } + + // 5. Return result. + return JS::NonnullGCPtr { *result }; +} + +// https://w3c.github.io/webcrypto/#hmac-operations +WebIDL::ExceptionOr HMAC::get_key_length(AlgorithmParams const& params) +{ + auto const& normalized_derived_key_algorithm = static_cast(params); + WebIDL::UnsignedLong length; + + // 1. If the length member of normalizedDerivedKeyAlgorithm is not present: + if (!normalized_derived_key_algorithm.length.has_value()) { + // Let length be the block size in bits of the hash function identified by the hash member of + // normalizedDerivedKeyAlgorithm. + length = TRY(hmac_hash_block_size(m_realm, normalized_derived_key_algorithm.hash)); + } + + // Otherwise, if the length member of normalizedDerivedKeyAlgorithm is non-zero: + else if (normalized_derived_key_algorithm.length.value() > 0) { + // Let length be equal to the length member of normalizedDerivedKeyAlgorithm. + length = normalized_derived_key_algorithm.length.value(); + } + + // Otherwise: + else { + // throw a TypeError. + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Invalid key length"sv }; + } + + // 2. Return length. + return JS::Value(length); +} + } diff --git a/Libraries/LibWeb/Crypto/CryptoAlgorithms.h b/Libraries/LibWeb/Crypto/CryptoAlgorithms.h index 7daf7ba59fe..9a3de6f3cc2 100644 --- a/Libraries/LibWeb/Crypto/CryptoAlgorithms.h +++ b/Libraries/LibWeb/Crypto/CryptoAlgorithms.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2024, Andrew Kaster * Copyright (c) 2024, stelar7 + * Copyright (c) 2024, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -17,6 +18,7 @@ #include #include #include +#include namespace Web::Crypto { @@ -260,6 +262,40 @@ struct AesDerivedKeyParams : public AlgorithmParams { static JS::ThrowCompletionOr> from_value(JS::VM&, JS::Value); }; +// https://w3c.github.io/webcrypto/#hmac-importparams +struct HmacImportParams : public AlgorithmParams { + virtual ~HmacImportParams() override; + + HmacImportParams(String name, HashAlgorithmIdentifier hash, Optional length) + : AlgorithmParams(move(name)) + , hash(move(hash)) + , length(length) + { + } + + HashAlgorithmIdentifier hash; + Optional length; + + static JS::ThrowCompletionOr> from_value(JS::VM&, JS::Value); +}; + +// https://w3c.github.io/webcrypto/#hmac-keygen-params +struct HmacKeyGenParams : public AlgorithmParams { + virtual ~HmacKeyGenParams() override; + + HmacKeyGenParams(String name, HashAlgorithmIdentifier hash, Optional length) + : AlgorithmParams(move(name)) + , hash(move(hash)) + , length(length) + { + } + + HashAlgorithmIdentifier hash; + Optional length; + + static JS::ThrowCompletionOr> from_value(JS::VM&, JS::Value); +}; + class AlgorithmMethods { public: virtual ~AlgorithmMethods(); @@ -489,6 +525,24 @@ private: } }; +class HMAC : public AlgorithmMethods { +public: + virtual WebIDL::ExceptionOr> sign(AlgorithmParams const&, JS::NonnullGCPtr, ByteBuffer const&) override; + virtual WebIDL::ExceptionOr verify(AlgorithmParams const&, JS::NonnullGCPtr, ByteBuffer const&, ByteBuffer const&) override; + virtual WebIDL::ExceptionOr, JS::NonnullGCPtr>> generate_key(AlgorithmParams const&, bool, Vector const&) override; + virtual WebIDL::ExceptionOr> import_key(AlgorithmParams const&, Bindings::KeyFormat, CryptoKey::InternalKeyData, bool, Vector const&) override; + virtual WebIDL::ExceptionOr> export_key(Bindings::KeyFormat, JS::NonnullGCPtr) override; + virtual WebIDL::ExceptionOr get_key_length(AlgorithmParams const&) override; + + static NonnullOwnPtr create(JS::Realm& realm) { return adopt_own(*new HMAC(realm)); } + +private: + explicit HMAC(JS::Realm& realm) + : AlgorithmMethods(realm) + { + } +}; + struct EcdhKeyDerivePrams : public AlgorithmParams { virtual ~EcdhKeyDerivePrams() override; diff --git a/Libraries/LibWeb/Crypto/KeyAlgorithms.cpp b/Libraries/LibWeb/Crypto/KeyAlgorithms.cpp index 3d5541f8b46..f07143a0ed9 100644 --- a/Libraries/LibWeb/Crypto/KeyAlgorithms.cpp +++ b/Libraries/LibWeb/Crypto/KeyAlgorithms.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2023, stelar7 * Copyright (c) 2024, Andrew Kaster + * Copyright (c) 2024, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -18,6 +19,7 @@ JS_DEFINE_ALLOCATOR(RsaKeyAlgorithm); JS_DEFINE_ALLOCATOR(RsaHashedKeyAlgorithm); JS_DEFINE_ALLOCATOR(EcKeyAlgorithm); JS_DEFINE_ALLOCATOR(AesKeyAlgorithm); +JS_DEFINE_ALLOCATOR(HmacKeyAlgorithm); template static JS::ThrowCompletionOr impl_from(JS::VM& vm, StringView Name) @@ -209,4 +211,38 @@ JS_DEFINE_NATIVE_FUNCTION(AesKeyAlgorithm::length_getter) return length; } +JS::NonnullGCPtr HmacKeyAlgorithm::create(JS::Realm& realm) +{ + return realm.create(realm); +} + +HmacKeyAlgorithm::HmacKeyAlgorithm(JS::Realm& realm) + : KeyAlgorithm(realm) +{ +} + +void HmacKeyAlgorithm::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + define_native_accessor(realm, "hash", hash_getter, {}, JS::Attribute::Enumerable | JS::Attribute::Configurable); +} + +void HmacKeyAlgorithm::visit_edges(JS::Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_hash); +} + +JS_DEFINE_NATIVE_FUNCTION(HmacKeyAlgorithm::hash_getter) +{ + auto* impl = TRY(impl_from(vm, "HmacKeyAlgorithm"sv)); + return TRY(Bindings::throw_dom_exception_if_needed(vm, [&] { return impl->hash(); })); +} + +JS_DEFINE_NATIVE_FUNCTION(HmacKeyAlgorithm::length_getter) +{ + auto* impl = TRY(impl_from(vm, "HmacKeyAlgorithm"sv)); + return TRY(Bindings::throw_dom_exception_if_needed(vm, [&] { return impl->length(); })); +} + } diff --git a/Libraries/LibWeb/Crypto/KeyAlgorithms.h b/Libraries/LibWeb/Crypto/KeyAlgorithms.h index 1618dbedbcc..78fb92be287 100644 --- a/Libraries/LibWeb/Crypto/KeyAlgorithms.h +++ b/Libraries/LibWeb/Crypto/KeyAlgorithms.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2023, stelar7 * Copyright (c) 2024, Andrew Kaster + * Copyright (c) 2024, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -145,4 +146,34 @@ private: u16 m_length; }; +// https://w3c.github.io/webcrypto/#HmacKeyAlgorithm-dictionary +struct HmacKeyAlgorithm : public KeyAlgorithm { + JS_OBJECT(HmacKeyAlgorithm, KeyAlgorithm); + JS_DECLARE_ALLOCATOR(HmacKeyAlgorithm); + +public: + static JS::NonnullGCPtr create(JS::Realm&); + + virtual ~HmacKeyAlgorithm() override = default; + + JS::GCPtr hash() const { return m_hash; } + void set_hash(JS::GCPtr hash) { m_hash = hash; } + + WebIDL::UnsignedLong length() const { return m_length; } + void set_length(WebIDL::UnsignedLong length) { m_length = length; } + +protected: + HmacKeyAlgorithm(JS::Realm&); + + virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Visitor&) override; + +private: + JS_DECLARE_NATIVE_FUNCTION(hash_getter); + JS_DECLARE_NATIVE_FUNCTION(length_getter); + + JS::GCPtr m_hash; + WebIDL::UnsignedLong m_length; +}; + } diff --git a/Libraries/LibWeb/Crypto/SubtleCrypto.cpp b/Libraries/LibWeb/Crypto/SubtleCrypto.cpp index 1e72bf8ed09..e99bad25d6e 100644 --- a/Libraries/LibWeb/Crypto/SubtleCrypto.cpp +++ b/Libraries/LibWeb/Crypto/SubtleCrypto.cpp @@ -830,12 +830,12 @@ SupportedAlgorithmsMap supported_algorithms() // FIXME: define_an_algorithm("get key length"_string, "AES-KW"_string); // https://w3c.github.io/webcrypto/#hmac-registration - // FIXME: define_an_algorithm("sign"_string, "HMAC"_string); - // FIXME: define_an_algorithm("verify"_string, "HMAC"_string); - // FIXME: define_an_algorithm("generateKey"_string, "HMAC"_string); - // FIXME: define_an_algorithm("importKey"_string, "HMAC"_string); - // FIXME: define_an_algorithm("exportKey"_string, "HMAC"_string); - // FIXME: define_an_algorithm("get key length"_string, "HMAC"_string); + define_an_algorithm("sign"_string, "HMAC"_string); + define_an_algorithm("verify"_string, "HMAC"_string); + define_an_algorithm("generateKey"_string, "HMAC"_string); + define_an_algorithm("importKey"_string, "HMAC"_string); + define_an_algorithm("exportKey"_string, "HMAC"_string); + define_an_algorithm("get key length"_string, "HMAC"_string); // https://w3c.github.io/webcrypto/#sha-registration define_an_algorithm("digest"_string, "SHA-1"_string); diff --git a/Tests/LibWeb/Text/expected/wpt-import/WebCryptoAPI/sign_verify/hmac.https.any.txt b/Tests/LibWeb/Text/expected/wpt-import/WebCryptoAPI/sign_verify/hmac.https.any.txt new file mode 100644 index 00000000000..3afc8357e42 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/WebCryptoAPI/sign_verify/hmac.https.any.txt @@ -0,0 +1,51 @@ +Summary + +Harness status: OK + +Rerun + +Found 41 tests + +41 Pass +Details +Result Test Name MessagePass setup +Pass HMAC with SHA-1 verification +Pass HMAC with SHA-256 verification +Pass HMAC with SHA-384 verification +Pass HMAC with SHA-512 verification +Pass HMAC with SHA-1 verification with altered signature after call +Pass HMAC with SHA-256 verification with altered signature after call +Pass HMAC with SHA-384 verification with altered signature after call +Pass HMAC with SHA-512 verification with altered signature after call +Pass HMAC with SHA-1 with altered plaintext after call +Pass HMAC with SHA-256 with altered plaintext after call +Pass HMAC with SHA-384 with altered plaintext after call +Pass HMAC with SHA-512 with altered plaintext after call +Pass HMAC with SHA-1 no verify usage +Pass HMAC with SHA-256 no verify usage +Pass HMAC with SHA-384 no verify usage +Pass HMAC with SHA-512 no verify usage +Pass HMAC with SHA-1 round trip +Pass HMAC with SHA-256 round trip +Pass HMAC with SHA-384 round trip +Pass HMAC with SHA-512 round trip +Pass HMAC with SHA-1 signing with wrong algorithm name +Pass HMAC with SHA-256 signing with wrong algorithm name +Pass HMAC with SHA-384 signing with wrong algorithm name +Pass HMAC with SHA-512 signing with wrong algorithm name +Pass HMAC with SHA-1 verifying with wrong algorithm name +Pass HMAC with SHA-256 verifying with wrong algorithm name +Pass HMAC with SHA-384 verifying with wrong algorithm name +Pass HMAC with SHA-512 verifying with wrong algorithm name +Pass HMAC with SHA-1 verification failure due to wrong plaintext +Pass HMAC with SHA-256 verification failure due to wrong plaintext +Pass HMAC with SHA-384 verification failure due to wrong plaintext +Pass HMAC with SHA-512 verification failure due to wrong plaintext +Pass HMAC with SHA-1 verification failure due to wrong signature +Pass HMAC with SHA-256 verification failure due to wrong signature +Pass HMAC with SHA-384 verification failure due to wrong signature +Pass HMAC with SHA-512 verification failure due to wrong signature +Pass HMAC with SHA-1 verification failure due to short signature +Pass HMAC with SHA-256 verification failure due to short signature +Pass HMAC with SHA-384 verification failure due to short signature +Pass HMAC with SHA-512 verification failure due to short signature diff --git a/Tests/LibWeb/Text/input/wpt-import/WebCryptoAPI/sign_verify/hmac.https.any.html b/Tests/LibWeb/Text/input/wpt-import/WebCryptoAPI/sign_verify/hmac.https.any.html new file mode 100644 index 00000000000..7d9c80d49f0 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/WebCryptoAPI/sign_verify/hmac.https.any.html @@ -0,0 +1,17 @@ + + +WebCryptoAPI: sign() and verify() Using HMAC + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/WebCryptoAPI/sign_verify/hmac.https.any.js b/Tests/LibWeb/Text/input/wpt-import/WebCryptoAPI/sign_verify/hmac.https.any.js new file mode 100644 index 00000000000..419bab05069 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/WebCryptoAPI/sign_verify/hmac.https.any.js @@ -0,0 +1,6 @@ +// META: title=WebCryptoAPI: sign() and verify() Using HMAC +// META: script=hmac_vectors.js +// META: script=hmac.js +// META: timeout=long + +run_test(); diff --git a/Tests/LibWeb/Text/input/wpt-import/WebCryptoAPI/sign_verify/hmac.js b/Tests/LibWeb/Text/input/wpt-import/WebCryptoAPI/sign_verify/hmac.js new file mode 100644 index 00000000000..f5e2ad2769c --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/WebCryptoAPI/sign_verify/hmac.js @@ -0,0 +1,351 @@ + +function run_test() { + setup({explicit_done: true}); + + var subtle = self.crypto.subtle; // Change to test prefixed implementations + + // When are all these tests really done? When all the promises they use have resolved. + var all_promises = []; + + // Source file hmac_vectors.js provides the getTestVectors method + // for the algorithm that drives these tests. + var testVectors = getTestVectors(); + + // Test verification first, because signing tests rely on that working + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify", "sign"]) + .then(function(vector) { + promise_test(function(test) { + var operation = subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, vector.signature, vector.plaintext) + .then(function(is_verified) { + assert_true(is_verified, "Signature verified"); + }, function(err) { + assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'"); + }); + + return operation; + }, vector.name + " verification"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verification"); + }); + + all_promises.push(promise); + }); + + // Test verification with an altered buffer after call + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify", "sign"]) + .then(function(vector) { + promise_test(function(test) { + var signature = copyBuffer(vector.signature); + var operation = subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, signature, vector.plaintext) + .then(function(is_verified) { + assert_true(is_verified, "Signature is not verified"); + }, function(err) { + assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'"); + }); + + signature[0] = 255 - signature[0]; + return operation; + }, vector.name + " verification with altered signature after call"); + }, function(err) { + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verification with altered signature after call"); + }); + + all_promises.push(promise); + }); + + // Check for successful verification even if plaintext is altered after call. + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify", "sign"]) + .then(function(vector) { + promise_test(function(test) { + var plaintext = copyBuffer(vector.plaintext); + var operation = subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, vector.signature, plaintext) + .then(function(is_verified) { + assert_true(is_verified, "Signature verified"); + }, function(err) { + assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'"); + }); + + plaintext[0] = 255 - plaintext[0]; + return operation; + }, vector.name + " with altered plaintext after call"); + }, function(err) { + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " with altered plaintext"); + }); + + all_promises.push(promise); + }); + + // Check for failures due to no "verify" usage. + testVectors.forEach(function(originalVector) { + var vector = Object.assign({}, originalVector); + + var promise = importVectorKeys(vector, ["sign"]) + .then(function(vector) { + promise_test(function(test) { + return subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, vector.signature, vector.plaintext) + .then(function(plaintext) { + assert_unreached("Should have thrown error for no verify usage in " + vector.name + ": " + err.message + "'"); + }, function(err) { + assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of '" + err.message + "'"); + }); + }, vector.name + " no verify usage"); + }, function(err) { + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " no verify usage"); + }); + + all_promises.push(promise); + }); + + // Check for successful signing and verification. + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify", "sign"]) + .then(function(vectors) { + promise_test(function(test) { + return subtle.sign({name: "HMAC", hash: vector.hash}, vector.key, vector.plaintext) + .then(function(signature) { + assert_true(equalBuffers(signature, vector.signature), "Signing did not give the expected output"); + // Can we get the verify the new signature? + return subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, signature, vector.plaintext) + .then(function(is_verified) { + assert_true(is_verified, "Round trip verifies"); + return signature; + }, function(err) { + assert_unreached("verify error for test " + vector.name + ": " + err.message + "'"); + }); + }); + }, vector.name + " round trip"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested signing or verifying + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " round trip"); + }); + + all_promises.push(promise); + }); + + // Test signing with the wrong algorithm + testVectors.forEach(function(vector) { + // Want to get the key for the wrong algorithm + var promise = subtle.generateKey({name: "ECDSA", namedCurve: "P-256", hash: "SHA-256"}, false, ["sign", "verify"]) + .then(function(wrongKey) { + return importVectorKeys(vector, ["verify", "sign"]) + .then(function(vectors) { + promise_test(function(test) { + var operation = subtle.sign({name: "HMAC", hash: vector.hash}, wrongKey.privateKey, vector.plaintext) + .then(function(signature) { + assert_unreached("Signing should not have succeeded for " + vector.name); + }, function(err) { + assert_equals(err.name, "InvalidAccessError", "Should have thrown InvalidAccessError instead of '" + err.message + "'"); + }); + + return operation; + }, vector.name + " signing with wrong algorithm name"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " signing with wrong algorithm name"); + }); + }, function(err) { + promise_test(function(test) { + assert_unreached("Generate wrong key for test " + vector.name + " failed: '" + err.message + "'"); + }, "generate wrong key step: " + vector.name + " signing with wrong algorithm name"); + }); + + all_promises.push(promise); + }); + + // Test verification with the wrong algorithm + testVectors.forEach(function(vector) { + // Want to get the key for the wrong algorithm + var promise = subtle.generateKey({name: "ECDSA", namedCurve: "P-256", hash: "SHA-256"}, false, ["sign", "verify"]) + .then(function(wrongKey) { + return importVectorKeys(vector, ["verify", "sign"]) + .then(function(vector) { + promise_test(function(test) { + var operation = subtle.verify({name: "HMAC", hash: vector.hash}, wrongKey.publicKey, vector.signature, vector.plaintext) + .then(function(signature) { + assert_unreached("Verifying should not have succeeded for " + vector.name); + }, function(err) { + assert_equals(err.name, "InvalidAccessError", "Should have thrown InvalidAccessError instead of '" + err.message + "'"); + }); + + return operation; + }, vector.name + " verifying with wrong algorithm name"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verifying with wrong algorithm name"); + }); + }, function(err) { + promise_test(function(test) { + assert_unreached("Generate wrong key for test " + vector.name + " failed: '" + err.message + "'"); + }, "generate wrong key step: " + vector.name + " verifying with wrong algorithm name"); + }); + + all_promises.push(promise); + }); + + // Verification should fail if the plaintext is changed + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify", "sign"]) + .then(function(vector) { + var plaintext = copyBuffer(vector.plaintext); + plaintext[0] = 255 - plaintext[0]; + promise_test(function(test) { + var operation = subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, vector.signature, plaintext) + .then(function(is_verified) { + assert_false(is_verified, "Signature is NOT verified"); + }, function(err) { + assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'"); + }); + + return operation; + }, vector.name + " verification failure due to wrong plaintext"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verification failure due to wrong plaintext"); + }); + + all_promises.push(promise); + }); + + // Verification should fail if the signature is changed + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify", "sign"]) + .then(function(vector) { + var signature = copyBuffer(vector.signature); + signature[0] = 255 - signature[0]; + promise_test(function(test) { + var operation = subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, signature, vector.plaintext) + .then(function(is_verified) { + assert_false(is_verified, "Signature is NOT verified"); + }, function(err) { + assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'"); + }); + + return operation; + }, vector.name + " verification failure due to wrong signature"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verification failure due to wrong signature"); + }); + + all_promises.push(promise); + }); + + // Verification should fail if the signature is wrong length + testVectors.forEach(function(vector) { + var promise = importVectorKeys(vector, ["verify", "sign"]) + .then(function(vector) { + var signature = vector.signature.slice(1); // Drop first byte + promise_test(function(test) { + var operation = subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, signature, vector.plaintext) + .then(function(is_verified) { + assert_false(is_verified, "Signature is NOT verified"); + }, function(err) { + assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'"); + }); + + return operation; + }, vector.name + " verification failure due to short signature"); + + }, function(err) { + // We need a failed test if the importVectorKey operation fails, so + // we know we never tested verification. + promise_test(function(test) { + assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''"); + }, "importVectorKeys step: " + vector.name + " verification failure due to short signature"); + }); + + all_promises.push(promise); + }); + + + + promise_test(function() { + return Promise.all(all_promises) + .then(function() {done();}) + .catch(function() {done();}) + }, "setup"); + + // A test vector has all needed fields for signing and verifying, EXCEPT that the + // key field may be null. This function replaces that null with the Correct + // CryptoKey object. + // + // Returns a Promise that yields an updated vector on success. + function importVectorKeys(vector, keyUsages) { + if (vector.key !== null) { + return new Promise(function(resolve, reject) { + resolve(vector); + }); + } else { + return subtle.importKey("raw", vector.keyBuffer, {name: "HMAC", hash: vector.hash}, false, keyUsages) + .then(function(key) { + vector.key = key; + return vector; + }); + } + } + + // Returns a copy of the sourceBuffer it is sent. + function copyBuffer(sourceBuffer) { + var source = new Uint8Array(sourceBuffer); + var copy = new Uint8Array(sourceBuffer.byteLength) + + for (var i=0; i