Skip to content

Commit

Permalink
feat: Add MD5 support into crypto.subtle.digest
Browse files Browse the repository at this point in the history
MD5 is not part of the WebCrypto API standard as a recommended algorithm to implement but we are adding support for it, which is allowed by the Web Crypto API.

The reason we are adding it is because some customers use legacy systems that require MD5 and it is better for performance if we implement it in native code rather than the customer implementing it is JS
  • Loading branch information
JakeChampion committed Apr 21, 2023
1 parent cdf2fee commit 9c8efab
Show file tree
Hide file tree
Showing 8 changed files with 8,367 additions and 134 deletions.
130 changes: 72 additions & 58 deletions c-dependencies/js-compute-runtime/builtins/crypto-algorithm.cpp
Expand Up @@ -32,7 +32,9 @@ const EVP_MD *createDigestAlgorithm(JSContext *cx, JS::HandleObject key) {
}

std::string_view name(name_chars.get(), name_length);
if (name == "SHA-1") {
if (name == "MD5") {
return EVP_md5();
} else if (name == "SHA-1") {
return EVP_sha1();
} else if (name == "SHA-224") {
return EVP_sha224();
Expand Down Expand Up @@ -282,62 +284,6 @@ std::unique_ptr<CryptoKeyRSAComponents> createRSAPrivateKeyFromJWK(JSContext *cx
return privateKeyComponents;
}

const char *algorithmName(CryptoAlgorithmIdentifier algorithm) {
switch (algorithm) {
case CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5: {
return "RSASSA-PKCS1-v1_5";
}
case CryptoAlgorithmIdentifier::RSA_PSS: {
return "RSA-PSS";
}
case CryptoAlgorithmIdentifier::RSA_OAEP: {
return "RSA-OAEP";
}
case CryptoAlgorithmIdentifier::ECDSA: {
return "ECDSA";
}
case CryptoAlgorithmIdentifier::ECDH: {
return "ECDH";
}
case CryptoAlgorithmIdentifier::AES_CTR: {
return "AES-CTR";
}
case CryptoAlgorithmIdentifier::AES_CBC: {
return "AES-CBC";
}
case CryptoAlgorithmIdentifier::AES_GCM: {
return "AES-GCM";
}
case CryptoAlgorithmIdentifier::AES_KW: {
return "AES-KW";
}
case CryptoAlgorithmIdentifier::HMAC: {
return "HMAC";
}
case CryptoAlgorithmIdentifier::SHA_1: {
return "SHA-1";
}
case CryptoAlgorithmIdentifier::SHA_256: {
return "SHA-256";
}
case CryptoAlgorithmIdentifier::SHA_384: {
return "SHA-384";
}
case CryptoAlgorithmIdentifier::SHA_512: {
return "SHA-512";
}
case CryptoAlgorithmIdentifier::HKDF: {
return "HKDF";
}
case CryptoAlgorithmIdentifier::PBKDF2: {
return "PBKDF2";
}
default: {
MOZ_ASSERT_UNREACHABLE("Unknown `CryptoAlgorithmIdentifier` value");
}
}
}

// Web Crypto API uses DOMExceptions to indicate errors
// We are adding the fields which are tested for in Web Platform Tests
// TODO: Implement DOMExceptions class and use that instead of duck-typing on an Error instance
Expand Down Expand Up @@ -445,6 +391,8 @@ JS::Result<CryptoAlgorithmIdentifier> normalizeIdentifier(JSContext *cx, JS::Han
return CryptoAlgorithmIdentifier::AES_KW;
} else if (algorithm == "HMAC") {
return CryptoAlgorithmIdentifier::HMAC;
} else if (algorithm == "MD5") {
return CryptoAlgorithmIdentifier::MD5;
} else if (algorithm == "SHA-1") {
return CryptoAlgorithmIdentifier::SHA_1;
} else if (algorithm == "SHA-256") {
Expand All @@ -465,6 +413,65 @@ JS::Result<CryptoAlgorithmIdentifier> normalizeIdentifier(JSContext *cx, JS::Han
}
} // namespace

const char *algorithmName(CryptoAlgorithmIdentifier algorithm) {
switch (algorithm) {
case CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5: {
return "RSASSA-PKCS1-v1_5";
}
case CryptoAlgorithmIdentifier::RSA_PSS: {
return "RSA-PSS";
}
case CryptoAlgorithmIdentifier::RSA_OAEP: {
return "RSA-OAEP";
}
case CryptoAlgorithmIdentifier::ECDSA: {
return "ECDSA";
}
case CryptoAlgorithmIdentifier::ECDH: {
return "ECDH";
}
case CryptoAlgorithmIdentifier::AES_CTR: {
return "AES-CTR";
}
case CryptoAlgorithmIdentifier::AES_CBC: {
return "AES-CBC";
}
case CryptoAlgorithmIdentifier::AES_GCM: {
return "AES-GCM";
}
case CryptoAlgorithmIdentifier::AES_KW: {
return "AES-KW";
}
case CryptoAlgorithmIdentifier::HMAC: {
return "HMAC";
}
case CryptoAlgorithmIdentifier::MD5: {
return "MD5";
}
case CryptoAlgorithmIdentifier::SHA_1: {
return "SHA-1";
}
case CryptoAlgorithmIdentifier::SHA_256: {
return "SHA-256";
}
case CryptoAlgorithmIdentifier::SHA_384: {
return "SHA-384";
}
case CryptoAlgorithmIdentifier::SHA_512: {
return "SHA-512";
}
case CryptoAlgorithmIdentifier::HKDF: {
return "HKDF";
}
case CryptoAlgorithmIdentifier::PBKDF2: {
return "PBKDF2";
}
default: {
MOZ_ASSERT_UNREACHABLE("Unknown `CryptoAlgorithmIdentifier` value");
}
}
}

// clang-format off
/// This table is from https://w3c.github.io/webcrypto/#h-note-15
// | Algorithm | encrypt | decrypt | sign | verify | digest | generateKey | deriveKey | deriveBits | importKey | exportKey | wrapKey | unwrapKey |
Expand Down Expand Up @@ -499,9 +506,13 @@ std::unique_ptr<CryptoAlgorithmDigest> CryptoAlgorithmDigest::normalize(JSContex

// The table listed at https://w3c.github.io/webcrypto/#h-note-15 is what defines which algorithms support which operations
// SHA-1, SHA-256, SHA-384, and SHA-512 are the only algorithms which support the digest operation
// We also support MD5 as an extra implementor defined algorithm

// Note: The specification states that none of the SHA algorithms take any parameters -- https://w3c.github.io/webcrypto/#sha-registration
switch (identifier) {
case CryptoAlgorithmIdentifier::MD5: {
return std::make_unique<CryptoAlgorithmMD5>();
}
case CryptoAlgorithmIdentifier::SHA_1: {
return std::make_unique<CryptoAlgorithmSHA1>();
}
Expand Down Expand Up @@ -991,7 +1002,7 @@ JSObject *CryptoAlgorithmRSASSA_PKCS1_v1_5_Import::toObject(JSContext *cx) {
// Set the hash attribute of algorithm to the hash member of normalizedAlgorithm.
JS::RootedObject hash(cx, JS_NewObject(cx, nullptr));

auto hash_name = JS_NewStringCopyZ(cx, algorithmName(this->hashIdentifier));
auto hash_name = JS_NewStringCopyZ(cx, builtins::algorithmName(this->hashIdentifier));
if (!hash_name) {
return nullptr;
}
Expand All @@ -1006,6 +1017,9 @@ JSObject *CryptoAlgorithmRSASSA_PKCS1_v1_5_Import::toObject(JSContext *cx) {
return algorithm;
}

JSObject *CryptoAlgorithmMD5::digest(JSContext *cx, std::span<uint8_t> data) {
return ::builtins::digest(cx, data, EVP_md5(), MD5_DIGEST_LENGTH);
}
JSObject *CryptoAlgorithmSHA1::digest(JSContext *cx, std::span<uint8_t> data) {
return ::builtins::digest(cx, data, EVP_sha1(), SHA_DIGEST_LENGTH);
}
Expand Down
11 changes: 11 additions & 0 deletions c-dependencies/js-compute-runtime/builtins/crypto-algorithm.h
Expand Up @@ -6,6 +6,7 @@
#include "crypto-key.h"
#include "json-web-key.h"
#include "openssl/evp.h"
#include <openssl/md5.h>

namespace builtins {

Expand All @@ -22,6 +23,7 @@ enum class CryptoAlgorithmIdentifier : uint8_t {
AES_GCM,
AES_KW,
HMAC,
MD5,
SHA_1,
SHA_256,
SHA_384,
Expand All @@ -30,6 +32,8 @@ enum class CryptoAlgorithmIdentifier : uint8_t {
PBKDF2
};

const char *algorithmName(CryptoAlgorithmIdentifier algorithm);

/// The base class that all algorithm implementations should derive from.
class CryptoAlgorithm {
public:
Expand Down Expand Up @@ -104,6 +108,13 @@ class CryptoAlgorithmDigest : public CryptoAlgorithm {
static std::unique_ptr<CryptoAlgorithmDigest> normalize(JSContext *cx, JS::HandleValue value);
};

class CryptoAlgorithmMD5 final : public CryptoAlgorithmDigest {
public:
const char *name() const noexcept override { return "MD5"; };
CryptoAlgorithmIdentifier identifier() override { return CryptoAlgorithmIdentifier::MD5; };
JSObject *digest(JSContext *cx, std::span<uint8_t>) override;
};

class CryptoAlgorithmSHA1 final : public CryptoAlgorithmDigest {
public:
const char *name() const noexcept override { return "SHA-1"; };
Expand Down
76 changes: 0 additions & 76 deletions c-dependencies/js-compute-runtime/builtins/crypto-key.cpp
Expand Up @@ -6,82 +6,6 @@

namespace builtins {

namespace {
const char *algorithmName(CryptoAlgorithmIdentifier algorithm) {
const char *result = nullptr;
switch (algorithm) {
case CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5: {
result = "RSASSA-PKCS1-v1_5";
break;
}
case CryptoAlgorithmIdentifier::RSA_PSS: {
result = "RSA-PSS";
break;
}
case CryptoAlgorithmIdentifier::RSA_OAEP: {
result = "RSA-OAEP";
break;
}
case CryptoAlgorithmIdentifier::ECDSA: {
result = "ECDSA";
break;
}
case CryptoAlgorithmIdentifier::ECDH: {
result = "ECDH";
break;
}
case CryptoAlgorithmIdentifier::AES_CTR: {
result = "AES-CTR";
break;
}
case CryptoAlgorithmIdentifier::AES_CBC: {
result = "AES-CBC";
break;
}
case CryptoAlgorithmIdentifier::AES_GCM: {
result = "AES-GCM";
break;
}
case CryptoAlgorithmIdentifier::AES_KW: {
result = "AES-KW";
break;
}
case CryptoAlgorithmIdentifier::HMAC: {
result = "HMAC";
break;
}
case CryptoAlgorithmIdentifier::SHA_1: {
result = "SHA-1";
break;
}
case CryptoAlgorithmIdentifier::SHA_256: {
result = "SHA-256";
break;
}
case CryptoAlgorithmIdentifier::SHA_384: {
result = "SHA-384";
break;
}
case CryptoAlgorithmIdentifier::SHA_512: {
result = "SHA-512";
break;
}
case CryptoAlgorithmIdentifier::HKDF: {
result = "HKDF";
break;
}
case CryptoAlgorithmIdentifier::PBKDF2: {
result = "PBKDF2";
break;
}
default: {
MOZ_ASSERT_UNREACHABLE("Unknown `CryptoAlgorithmIdentifier` value");
}
}
return result;
}
} // namespace

CryptoKeyUsages::CryptoKeyUsages(uint8_t mask) { this->mask = mask; };
CryptoKeyUsages::CryptoKeyUsages(bool encrypt, bool decrypt, bool sign, bool verify,
bool derive_key, bool derive_bits, bool wrap_key,
Expand Down
Expand Up @@ -25,6 +25,7 @@ digest(algorithm, data)

- `algorithm`
- : This may be a string or an object with a single property `name` that is a string. The string names the hash function to use. Supported values are:
- `"MD5"` (but don't use this in cryptographic applications)
- `"SHA-1"` (but don't use this in cryptographic applications)
- `"SHA-256"`
- `"SHA-384"`
Expand Down
8 changes: 8 additions & 0 deletions integration-tests/js-compute/fixtures/crypto/bin/index.js
Expand Up @@ -562,6 +562,14 @@ routes.set("/crypto.subtle", async () => {
}
// happy paths
{
// "MD5"
routes.set("/crypto.subtle.digest/md5", async () => {
const result = new Uint8Array(await crypto.subtle.digest("md5", new Uint8Array));
const expected = new Uint8Array([212, 29, 140, 217, 143, 0, 178, 4, 233, 128, 9, 152, 236, 248, 66, 126]);
error = assert(result, expected, "result deep equals expected");
if (error) { return error; }
return pass();
});
// "SHA-1"
routes.set("/crypto.subtle.digest/sha-1", async () => {
const result = new Uint8Array(await crypto.subtle.digest("sha-1", new Uint8Array));
Expand Down

0 comments on commit 9c8efab

Please sign in to comment.