Skip to content

Commit a8986d6

Browse files
Jake ChampionJakeChampion
authored andcommitted
chore: refactor how we implement crypto algorithms to make it simpler to add new algorithm implementations in the future
instead of having everything defined in subtle-crypto.cpp/.h - we are breaking the algorithms out into their own file and creating base classes which all the algorthims will derive from. Eventually there will be base classes for all the different algorithm operations such as importing/exporting, encrypting/decrypting, signing/veryifing, wraping/unrwapping etc. this refactor also helps keep subtle-crypto.cpp to contain only the logic required for following the web-crypto specification
1 parent 5a6302e commit a8986d6

File tree

5 files changed

+327
-400
lines changed

5 files changed

+327
-400
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
#include "openssl/sha.h"
2+
#include <iostream>
3+
#include <span>
4+
5+
#include "crypto-algorithm.h"
6+
7+
namespace builtins {
8+
9+
namespace {
10+
11+
// Web Crypto API uses DOMExceptions to indicate errors
12+
// We are adding the fields which are tested for in Web Platform Tests
13+
// TODO: Implement DOMExceptions class and use that instead of duck-typing on an Error instance
14+
void convertErrorToNotSupported(JSContext *cx) {
15+
MOZ_ASSERT(JS_IsExceptionPending(cx));
16+
JS::RootedValue exn(cx);
17+
if (!JS_GetPendingException(cx, &exn)) {
18+
return;
19+
}
20+
MOZ_ASSERT(exn.isObject());
21+
JS::RootedObject error(cx, &exn.toObject());
22+
JS::RootedValue name(cx, JS::StringValue(JS_NewStringCopyZ(cx, "NotSupportedError")));
23+
JS_SetProperty(cx, error, "name", name);
24+
JS::RootedValue code(cx, JS::NumberValue(9));
25+
JS_SetProperty(cx, error, "code", code);
26+
}
27+
28+
// This implements the first section of
29+
// https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm which is shared
30+
// across all the diffent algorithms, but importantly does not implement the parts to do with the
31+
// chosen `op` (operation) the `op` parts are handled in the specialized `normalize` functions on
32+
// the concrete classes which derive from CryptoAlgorithm such as CryptoAlgorithmDigest.
33+
JS::Result<CryptoAlgorithmIdentifier> normalizeIdentifier(JSContext *cx, JS::HandleValue value) {
34+
35+
// The specification states:
36+
// --------
37+
// If alg is an instance of a DOMString:
38+
// Return the result of running the normalize an algorithm algorithm,
39+
// with the alg set to a new Algorithm dictionary whose name attribute
40+
// is alg, and with the op set to op.
41+
// --------
42+
// Instead of doing that, we operate on the string and not the dictionary.
43+
// If we see a dictionary (JSObject), we pull the name attribute out
44+
// and coerce it's value to a String.
45+
// The reason we chose this direct is because we only need this one field
46+
// from the provided dictionary, so we store the field on it's own and not
47+
// in a JSObject which would take up more memory.
48+
49+
// 1. Let registeredAlgorithms be the associative container stored at the op key of
50+
// supportedAlgorithms.
51+
// 2. Let initialAlg be the result of converting the ECMAScript object represented by alg to the
52+
// IDL dictionary type Algorithm, as defined by [WebIDL].
53+
// 3. If an error occurred, return the error and terminate this algorithm.
54+
// 4. Let algName be the value of the name attribute of initialAlg.
55+
JS::Rooted<JSString *> algName(cx);
56+
if (value.isObject()) {
57+
JS::Rooted<JSObject *> params(cx, &value.toObject());
58+
JS::Rooted<JS::Value> name_val(cx);
59+
if (!JS_GetProperty(cx, params, "name", &name_val)) {
60+
return JS::Result<CryptoAlgorithmIdentifier>(JS::Error());
61+
}
62+
algName.set(JS::ToString(cx, name_val));
63+
} else {
64+
algName.set(JS::ToString(cx, value));
65+
}
66+
// If `algName` is falsey, it means the call to JS::ToString failed.
67+
// In that scenario, we should already have an exception, which is why we are not creating our own
68+
// one.
69+
if (!algName) {
70+
return JS::Result<CryptoAlgorithmIdentifier>(JS::Error());
71+
}
72+
73+
// TODO: We convert from JSString to std::string quite a lot in the codebase, should we pull this
74+
// logic out into a new function?
75+
size_t algorithmLen;
76+
JS::UniqueChars algorithmChars = encode(cx, algName, &algorithmLen);
77+
if (!algorithmChars) {
78+
return JS::Result<CryptoAlgorithmIdentifier>(JS::Error());
79+
}
80+
std::string algorithm(algorithmChars.get(), algorithmLen);
81+
82+
// 5. If registeredAlgorithms contains a key that is a case-insensitive string match for algName:
83+
// 5.1 Set algName to the value of the matching key.
84+
// 5.2 Let desiredType be the IDL dictionary type stored at algName in registeredAlgorithms.
85+
// Note: We do not implement 5.2 here, it is instead implemented in the specialized `normalize`
86+
// functions.
87+
std::transform(algorithm.begin(), algorithm.end(), algorithm.begin(),
88+
[](unsigned char c) { return std::toupper(c); });
89+
if (algorithm == "RSASSA-PKCS1-V1_5") {
90+
return CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5;
91+
} else if (algorithm == "RSA-PSS") {
92+
return CryptoAlgorithmIdentifier::RSA_PSS;
93+
} else if (algorithm == "RSA-OAEP") {
94+
return CryptoAlgorithmIdentifier::RSA_OAEP;
95+
} else if (algorithm == "ECDSA") {
96+
return CryptoAlgorithmIdentifier::ECDSA;
97+
} else if (algorithm == "ECDH") {
98+
return CryptoAlgorithmIdentifier::ECDH;
99+
} else if (algorithm == "AES-CTR") {
100+
return CryptoAlgorithmIdentifier::AES_CTR;
101+
} else if (algorithm == "AES-CBC") {
102+
return CryptoAlgorithmIdentifier::AES_CBC;
103+
} else if (algorithm == "AES-GCM") {
104+
return CryptoAlgorithmIdentifier::AES_GCM;
105+
} else if (algorithm == "AES-KW") {
106+
return CryptoAlgorithmIdentifier::AES_KW;
107+
} else if (algorithm == "HMAC") {
108+
return CryptoAlgorithmIdentifier::HMAC;
109+
} else if (algorithm == "SHA-1") {
110+
return CryptoAlgorithmIdentifier::SHA_1;
111+
} else if (algorithm == "SHA-256") {
112+
return CryptoAlgorithmIdentifier::SHA_256;
113+
} else if (algorithm == "SHA-384") {
114+
return CryptoAlgorithmIdentifier::SHA_384;
115+
} else if (algorithm == "SHA-512") {
116+
return CryptoAlgorithmIdentifier::SHA_512;
117+
} else if (algorithm == "HKDF") {
118+
return CryptoAlgorithmIdentifier::HKDF;
119+
} else if (algorithm == "PBKDF2") {
120+
return CryptoAlgorithmIdentifier::PBKDF2;
121+
} else {
122+
// Otherwise: Return a new NotSupportedError and terminate this algorithm.
123+
JS_ReportErrorUTF8(cx, "Algorithm: Unrecognized name");
124+
return JS::Result<CryptoAlgorithmIdentifier>(JS::Error());
125+
}
126+
}
127+
} // namespace
128+
129+
// clang-format off
130+
/// This table is from https://w3c.github.io/webcrypto/#h-note-15
131+
// | Algorithm | encrypt | decrypt | sign | verify | digest | generateKey | deriveKey | deriveBits | importKey | exportKey | wrapKey | unwrapKey |
132+
// | RSASSA-PKCS1-v1_5 | | | ✔ | ✔ | | ✔ | | | ✔ | ✔ | | |
133+
// | RSA-PSS | | | ✔ | ✔ | | ✔ | | | ✔ | ✔ | | |
134+
// | RSA-OAEP | ✔ | ✔ | | | | ✔ | | | ✔ | ✔ | ✔ | ✔ |
135+
// | ECDSA | | | ✔ | ✔ | | ✔ | | | ✔ | ✔ | | |
136+
// | ECDH | | | | | | ✔ | ✔ | ✔ | ✔ | ✔ | | |
137+
// | AES-CTR | ✔ | ✔ | | | | ✔ | | | ✔ | ✔ | ✔ | ✔ |
138+
// | AES-CBC | ✔ | ✔ | | | | ✔ | | | ✔ | ✔ | ✔ | ✔ |
139+
// | AES-GCM | ✔ | ✔ | | | | ✔ | | | ✔ | ✔ | ✔ | ✔ |
140+
// | AES-KW | | | | | | ✔ | | | ✔ | ✔ | ✔ | ✔ |
141+
// | HMAC | | | ✔ | ✔ | | ✔ | | | ✔ | ✔ | | |
142+
// | SHA-1 | | | | | ✔ | | | | | | | |
143+
// | SHA-256 | | | | | ✔ | | | | | | | |
144+
// | SHA-384 | | | | | ✔ | | | | | | | |
145+
// | SHA-512 | | | | | ✔ | | | | | | | |
146+
// | HKDF | | | | | | | ✔ | ✔ | ✔ | | | |
147+
// | PBKDF2 | | | | | | | ✔ | ✔ | ✔ | | | |
148+
//clang-format on
149+
150+
std::unique_ptr<CryptoAlgorithmDigest> CryptoAlgorithmDigest::normalize(JSContext *cx,
151+
JS::HandleValue value) {
152+
// Do steps 1 through 5.1 of https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm
153+
auto identifierResult = normalizeIdentifier(cx, value);
154+
if (identifierResult.isErr()) {
155+
// If we are here, this means either the identifier could not be coerced to a String or was not recognized
156+
// In both those scenarios an exception will have already been created, which is why we are not creating one here.
157+
return nullptr;
158+
}
159+
auto identifier = identifierResult.unwrap();
160+
161+
// The table listed at https://w3c.github.io/webcrypto/#h-note-15 is what defines which algorithms support which operations
162+
// SHA-1, SHA-256, SHA-384, and SHA-512 are the only algorithms which support the digest operation
163+
164+
// Note: The specification states that none of the SHA algorithms take any parameters -- https://w3c.github.io/webcrypto/#sha-registration
165+
switch (identifier) {
166+
case CryptoAlgorithmIdentifier::SHA_1: {
167+
return std::make_unique<CryptoAlgorithmSHA1>();
168+
}
169+
case CryptoAlgorithmIdentifier::SHA_256: {
170+
return std::make_unique<CryptoAlgorithmSHA256>();
171+
}
172+
case CryptoAlgorithmIdentifier::SHA_384: {
173+
return std::make_unique<CryptoAlgorithmSHA384>();
174+
}
175+
case CryptoAlgorithmIdentifier::SHA_512: {
176+
return std::make_unique<CryptoAlgorithmSHA512>();
177+
}
178+
default: {
179+
JS_ReportErrorASCII(cx, "Supplied algorithm does not support the digest operation");
180+
convertErrorToNotSupported(cx);
181+
return nullptr;
182+
}
183+
}
184+
};
185+
186+
namespace {
187+
// This implements https://w3c.github.io/webcrypto/#sha-operations for all
188+
// the SHA algorithms that we support.
189+
JSObject *digest(JSContext *cx, std::span<uint8_t> data, const EVP_MD * algorithm, size_t buffer_size) {
190+
unsigned int size;
191+
auto buf = static_cast<unsigned char *>(JS_malloc(cx, buffer_size));
192+
if (!buf) {
193+
JS_ReportOutOfMemory(cx);
194+
return nullptr;
195+
}
196+
if (!EVP_Digest(data.data(), data.size(), buf, &size, algorithm, NULL)) {
197+
// 2. If performing the operation results in an error, then throw an OperationError.
198+
// TODO: Change to an OperationError DOMException
199+
JS_ReportErrorUTF8(cx, "SubtleCrypto.digest: failed to create digest");
200+
return nullptr;
201+
}
202+
// 3. Return a new ArrayBuffer containing result.
203+
JS::RootedObject array_buffer(cx);
204+
array_buffer.set(JS::NewArrayBufferWithContents(cx, size, buf));
205+
if (!array_buffer) {
206+
JS_ReportOutOfMemory(cx);
207+
return nullptr;
208+
}
209+
return array_buffer;
210+
};
211+
}
212+
213+
JSObject *CryptoAlgorithmSHA1::digest(JSContext *cx, std::span<uint8_t> data) {
214+
return ::builtins::digest(cx, data, EVP_sha1(), SHA_DIGEST_LENGTH);
215+
}
216+
JSObject *CryptoAlgorithmSHA256::digest(JSContext *cx, std::span<uint8_t> data) {
217+
return ::builtins::digest(cx, data, EVP_sha256(), SHA256_DIGEST_LENGTH);
218+
}
219+
JSObject *CryptoAlgorithmSHA384::digest(JSContext *cx, std::span<uint8_t> data) {
220+
return ::builtins::digest(cx, data, EVP_sha384(), SHA384_DIGEST_LENGTH);
221+
}
222+
JSObject *CryptoAlgorithmSHA512::digest(JSContext *cx, std::span<uint8_t> data) {
223+
return ::builtins::digest(cx, data, EVP_sha512(), SHA512_DIGEST_LENGTH);
224+
}
225+
226+
} // namespace builtins
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#ifndef JS_COMPUTE_RUNTIME_CRYPTO_ALGORITHM_H
2+
#define JS_COMPUTE_RUNTIME_CRYPTO_ALGORITHM_H
3+
#include <span>
4+
5+
#include "builtin.h"
6+
#include "openssl/evp.h"
7+
8+
namespace builtins {
9+
10+
// We are defining all the algorithms from
11+
// https://w3c.github.io/webcrypto/#issue-container-generatedID-15
12+
enum class CryptoAlgorithmIdentifier {
13+
RSASSA_PKCS1_v1_5,
14+
RSA_PSS,
15+
RSA_OAEP,
16+
ECDSA,
17+
ECDH,
18+
AES_CTR,
19+
AES_CBC,
20+
AES_GCM,
21+
AES_KW,
22+
HMAC,
23+
SHA_1,
24+
SHA_256,
25+
SHA_384,
26+
SHA_512,
27+
HKDF,
28+
PBKDF2
29+
};
30+
31+
/// The base class that all algorithm implementations should derive from.
32+
class CryptoAlgorithm {
33+
public:
34+
virtual ~CryptoAlgorithm() = default;
35+
36+
virtual const char *name() const noexcept = 0;
37+
virtual CryptoAlgorithmIdentifier identifier() = 0;
38+
};
39+
40+
class CryptoAlgorithmDigest : public CryptoAlgorithm {
41+
public:
42+
virtual JSObject *digest(JSContext *cx, std::span<uint8_t>) = 0;
43+
static std::unique_ptr<CryptoAlgorithmDigest> normalize(JSContext *cx, JS::HandleValue value);
44+
};
45+
46+
class CryptoAlgorithmSHA1 final : public CryptoAlgorithmDigest {
47+
public:
48+
const char *name() const noexcept override { return "SHA-1"; };
49+
CryptoAlgorithmIdentifier identifier() override { return CryptoAlgorithmIdentifier::SHA_1; };
50+
JSObject *digest(JSContext *cx, std::span<uint8_t>) override;
51+
};
52+
53+
class CryptoAlgorithmSHA256 final : public CryptoAlgorithmDigest {
54+
public:
55+
const char *name() const noexcept override { return "SHA-256"; };
56+
CryptoAlgorithmIdentifier identifier() override { return CryptoAlgorithmIdentifier::SHA_256; };
57+
JSObject *digest(JSContext *cx, std::span<uint8_t>) override;
58+
};
59+
60+
class CryptoAlgorithmSHA384 final : public CryptoAlgorithmDigest {
61+
public:
62+
const char *name() const noexcept override { return "SHA-384"; };
63+
CryptoAlgorithmIdentifier identifier() override { return CryptoAlgorithmIdentifier::SHA_384; };
64+
JSObject *digest(JSContext *cx, std::span<uint8_t>) override;
65+
};
66+
67+
class CryptoAlgorithmSHA512 final : public CryptoAlgorithmDigest {
68+
public:
69+
const char *name() const noexcept override { return "SHA-512"; };
70+
CryptoAlgorithmIdentifier identifier() override { return CryptoAlgorithmIdentifier::SHA_512; };
71+
JSObject *digest(JSContext *cx, std::span<uint8_t>) override;
72+
};
73+
74+
} // namespace builtins
75+
#endif

0 commit comments

Comments
 (0)