From f670c68ded838a30d0db341636585a67b9f9e9b5 Mon Sep 17 00:00:00 2001 From: Ben Wiederhake Date: Sun, 20 Oct 2024 06:10:36 +0200 Subject: [PATCH] LibWeb: Implement and test SubtleCrypto interface for HKDF operations This fixes several hundred if not thousands of WPT tests: https://wpt.live/WebCryptoAPI/derive_bits_keys/hkdf.https.any.html?1-1000 --- .../LibWeb/Crypto/CryptoAlgorithms.cpp | 129 ++++++++++++++++++ .../LibWeb/Crypto/CryptoAlgorithms.h | 33 +++++ .../Libraries/LibWeb/Crypto/SubtleCrypto.cpp | 5 + 3 files changed, 167 insertions(+) diff --git a/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp b/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp index 62b24afa7cc..bb481b08afa 100644 --- a/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp +++ b/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -226,6 +227,31 @@ JS::ThrowCompletionOr> AlgorithmParams::from_valu return adopt_own(*new AlgorithmParams { name_string }); } +HKDFParams::~HKDFParams() = default; + +JS::ThrowCompletionOr> HKDFParams::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_value.to_string(vm)); + + auto salt_value = TRY(object.get("salt")); + if (!salt_value.is_object() || !(is(salt_value.as_object()) || is(salt_value.as_object()) || is(salt_value.as_object()))) + return vm.throw_completion(JS::ErrorType::NotAnObjectOfType, "BufferSource"); + auto salt = TRY_OR_THROW_OOM(vm, WebIDL::get_buffer_source_copy(salt_value.as_object())); + + auto info_value = TRY(object.get("info")); + if (!info_value.is_object() || !(is(info_value.as_object()) || is(info_value.as_object()) || is(info_value.as_object()))) + return vm.throw_completion(JS::ErrorType::NotAnObjectOfType, "BufferSource"); + auto info = TRY_OR_THROW_OOM(vm, WebIDL::get_buffer_source_copy(info_value.as_object())); + + return adopt_own(*new HKDFParams { name, hash, salt, info }); +} + PBKDF2Params::~PBKDF2Params() = default; JS::ThrowCompletionOr> PBKDF2Params::from_value(JS::VM& vm, JS::Value value) @@ -939,6 +965,52 @@ WebIDL::ExceptionOr> RSAOAEP::export_key(Bindings:: return JS::NonnullGCPtr { *result }; } +// https://w3c.github.io/webcrypto/#hkdf-operations +WebIDL::ExceptionOr> HKDF::import_key(AlgorithmParams const&, Bindings::KeyFormat format, CryptoKey::InternalKeyData key_data, bool extractable, Vector const& key_usages) +{ + // 1. Let keyData be the key data to be imported. + + // 2. If format is "raw": + // (… see below …) + // Otherwise: + // throw a NotSupportedError. + if (format != Bindings::KeyFormat::Raw) { + return WebIDL::NotSupportedError::create(m_realm, "Only raw format is supported"_string); + } + + // 1. If usages contains a value that is not "deriveKey" or "deriveBits", then throw a SyntaxError. + for (auto& usage : key_usages) { + if (usage != Bindings::KeyUsage::Derivekey && usage != Bindings::KeyUsage::Derivebits) { + return WebIDL::SyntaxError::create(m_realm, MUST(String::formatted("Invalid key usage '{}'", idl_enum_to_string(usage)))); + } + } + + // 2. If extractable is not false, then throw a SyntaxError. + if (extractable) + return WebIDL::SyntaxError::create(m_realm, "extractable must be false"_string); + + // 3. Let key be a new CryptoKey representing the key data provided in keyData. + auto key = CryptoKey::create(m_realm, move(key_data)); + + // 4. Set the [[type]] internal slot of key to "secret". + key->set_type(Bindings::KeyType::Secret); + + // 5. Set the [[extractable]] internal slot of key to false. + key->set_extractable(false); + + // 6. Let algorithm be a new KeyAlgorithm object. + auto algorithm = KeyAlgorithm::create(m_realm); + + // 7. Set the name attribute of algorithm to "HKDF". + algorithm->set_name("HKDF"_string); + + // 8. Set the [[algorithm]] internal slot of key to algorithm. + key->set_algorithm(algorithm); + + // 9. Return key. + return key; +} + 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 @@ -1371,6 +1443,63 @@ WebIDL::ExceptionOr ED25519::verify([[maybe_unused]] AlgorithmParams return JS::Value(result); } +// https://w3c.github.io/webcrypto/#hkdf-operations +WebIDL::ExceptionOr> HKDF::derive_bits(AlgorithmParams const& params, JS::NonnullGCPtr key, Optional length_optional) +{ + auto& realm = *m_realm; + auto const& normalized_algorithm = static_cast(params); + + // 1. If length is null or zero, or is not a multiple of 8, then throw an OperationError. + auto length = length_optional.value_or(0); + + if (length == 0 || length % 8 != 0) + return WebIDL::OperationError::create(realm, "Length must be greater than 0 and divisible by 8"_string); + + // 2. Let extractKey be a key equal to n zero bits where n is the size of the output of the hash function described by the hash member of normalizedAlgorithm. + // (However, this variable is never directly used, and therefore pointless.) + + // 3. Let keyDerivationKey be the secret represented by [[handle]] internal slot of key as the message. + auto key_derivation_key = key->handle().get(); + + // 4. Let result be the result of performing the HKDF extract and then the HKDF expand step described in Section 2 of [RFC5869] using: + // * the hash member of normalizedAlgorithm as Hash, + // * keyDerivationKey as the input keying material, IKM, + // * the contents of the salt member of normalizedAlgorithm as salt, + // * the contents of the info member of normalizedAlgorithm as info, + // * length divided by 8 as the value of L, + // FIXME: salt null versus salt empty?! + auto const& hash_algorithm = TRY(normalized_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(m_realm->vm()); })); + ErrorOr result = Error::from_string_literal("noop error"); + if (hash_algorithm.equals_ignoring_ascii_case("SHA-1"sv)) { + result = ::Crypto::Hash::HKDF<::Crypto::Hash::SHA1>::derive_key(Optional(normalized_algorithm.salt), key_derivation_key, normalized_algorithm.info, length / 8); + } else if (hash_algorithm.equals_ignoring_ascii_case("SHA-256"sv)) { + result = ::Crypto::Hash::HKDF<::Crypto::Hash::SHA256>::derive_key(Optional(normalized_algorithm.salt), key_derivation_key, normalized_algorithm.info, length / 8); + } else if (hash_algorithm.equals_ignoring_ascii_case("SHA-384"sv)) { + result = ::Crypto::Hash::HKDF<::Crypto::Hash::SHA384>::derive_key(Optional(normalized_algorithm.salt), key_derivation_key, normalized_algorithm.info, length / 8); + } else if (hash_algorithm.equals_ignoring_ascii_case("SHA-512"sv)) { + result = ::Crypto::Hash::HKDF<::Crypto::Hash::SHA512>::derive_key(Optional(normalized_algorithm.salt), key_derivation_key, normalized_algorithm.info, length / 8); + } else { + return WebIDL::NotSupportedError::create(m_realm, MUST(String::formatted("Invalid hash function '{}'", hash_algorithm))); + } + + // 5. If the key derivation operation fails, then throw an OperationError. + if (result.is_error()) + return WebIDL::OperationError::create(realm, "Failed to derive key"_string); + + // 6. Return result + return JS::ArrayBuffer::create(realm, result.release_value()); +} + +WebIDL::ExceptionOr HKDF::get_key_length(AlgorithmParams const&) +{ + // 1. Return null. + return JS::js_null(); +} + WebIDL::ExceptionOr> PBKDF2::derive_bits(AlgorithmParams const& params, JS::NonnullGCPtr key, Optional length_optional) { auto& realm = *m_realm; diff --git a/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.h b/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.h index 09386594bab..fd52f8c9906 100644 --- a/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.h +++ b/Userland/Libraries/LibWeb/Crypto/CryptoAlgorithms.h @@ -37,6 +37,24 @@ struct AlgorithmParams { static JS::ThrowCompletionOr> from_value(JS::VM&, JS::Value); }; +// https://w3c.github.io/webcrypto/#hkdf-params +struct HKDFParams : public AlgorithmParams { + virtual ~HKDFParams() override; + HKDFParams(String name, HashAlgorithmIdentifier hash, ByteBuffer salt, ByteBuffer info) + : AlgorithmParams(move(name)) + , hash(move(hash)) + , salt(move(salt)) + , info(move(info)) + { + } + + HashAlgorithmIdentifier hash; + ByteBuffer salt; + ByteBuffer info; + + static JS::ThrowCompletionOr> from_value(JS::VM&, JS::Value); +}; + // https://w3c.github.io/webcrypto/#pbkdf2-params struct PBKDF2Params : public AlgorithmParams { virtual ~PBKDF2Params() override; @@ -232,6 +250,21 @@ private: } }; +class HKDF : public AlgorithmMethods { +public: + virtual WebIDL::ExceptionOr> import_key(AlgorithmParams const&, Bindings::KeyFormat, CryptoKey::InternalKeyData, bool, Vector const&) override; + virtual WebIDL::ExceptionOr> derive_bits(AlgorithmParams const&, JS::NonnullGCPtr, Optional) override; + virtual WebIDL::ExceptionOr get_key_length(AlgorithmParams const&) override; + + static NonnullOwnPtr create(JS::Realm& realm) { return adopt_own(*new HKDF(realm)); } + +private: + explicit HKDF(JS::Realm& realm) + : AlgorithmMethods(realm) + { + } +}; + class PBKDF2 : public AlgorithmMethods { public: virtual WebIDL::ExceptionOr> import_key(AlgorithmParams const&, Bindings::KeyFormat, CryptoKey::InternalKeyData, bool, Vector const&) override; diff --git a/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.cpp b/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.cpp index 5d593e5ee74..bc03d3fd8b3 100644 --- a/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.cpp +++ b/Userland/Libraries/LibWeb/Crypto/SubtleCrypto.cpp @@ -768,6 +768,11 @@ SupportedAlgorithmsMap supported_algorithms() define_an_algorithm("digest"_string, "SHA-384"_string); define_an_algorithm("digest"_string, "SHA-512"_string); + // https://w3c.github.io/webcrypto/#hkdf + define_an_algorithm("importKey"_string, "HKDF"_string); + define_an_algorithm("deriveBits"_string, "HKDF"_string); + define_an_algorithm("get key length"_string, "HKDF"_string); + // https://w3c.github.io/webcrypto/#pbkdf2 define_an_algorithm("importKey"_string, "PBKDF2"_string); define_an_algorithm("deriveBits"_string, "PBKDF2"_string);