Skip to content
File

Blob: src/workerd/api/crypto/aes.c++

37.5 KB
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#include "impl.h"
6#include "util.h"
7 
8#include <workerd/io/io-context.h>
9#include <workerd/jsg/jsvalue.h>
10 
11#include <openssl/aes.h>
12#include <openssl/base.h>
13#include <openssl/bn.h>
14#include <openssl/cipher.h>
15#include <openssl/mem.h>
16 
17#include <algorithm>
18#include <cstdint>
19 
20namespace workerd::api {
21namespace {
22auto lookupAesCbcType(uint bitLength) {
23 switch (bitLength) {
24 case 128:
25 return EVP_aes_128_cbc();
26 case 192:
27 return EVP_aes_192_cbc();
28 case 256:
29 return EVP_aes_256_cbc();
30 default:
31 KJ_FAIL_ASSERT("CryptoKey has invalid data length", bitLength);
32 // Assert because the data length must have come from a key we created!
33 }
34}
35 
36auto lookupAesGcmType(uint bitLength) {
37 switch (bitLength) {
38 case 128:
39 return EVP_aes_128_gcm();
40 case 192:
41 return EVP_aes_192_gcm();
42 case 256:
43 return EVP_aes_256_gcm();
44 default:
45 KJ_FAIL_ASSERT("CryptoKey has invalid data length", bitLength);
46 // Assert because the data length must have come from a key we created!
47 }
48}
49 
50// Ensure the tagLength passed to the AES-GCM algorithm is one of the allowed bit lengths.
51void validateAesGcmTagLength(int tagLength) {
52 switch (tagLength) {
53 case 32:
54 case 64:
55 case 96:
56 case 104:
57 case 112:
58 case 120:
59 case 128:
60 break;
61 default:
62 JSG_FAIL_REQUIRE(DOMOperationError, "Invalid AES-GCM tag length ", tagLength, ".");
63 }
64}
65 
66int decryptFinalHelper(kj::StringPtr algorithm,
67 size_t inputLength,
68 size_t outputLength,
69 EVP_CIPHER_CTX* cipherCtx,
70 kj::byte* out) {
71 // EVP_DecryptFinal_ex() failures can mean a mundane decryption failure, so we have to be careful
72 // with error handling when calling it. We can't use our usual OSSLCALL() macro, because that
73 // throws an unhelpful opaque OperationError.
74 
75 // Clear the error queue; who knows what kind of junk is in there.
76 ClearErrorOnReturn clearErrorOnReturn;
77 
78 int finalPlainSize = 0;
79 if (EVP_DecryptFinal_ex(cipherCtx, out, &finalPlainSize)) {
80 return finalPlainSize;
81 }
82 
83 // Decryption failure! Let's figure out what exception to throw.
84 
85 auto ec = clearErrorOnReturn.peekError();
86 
87 // If the error code is anything other than zero or BAD_DECRYPT, just throw an opaque
88 // OperationError for consistency with our OSSLCALL() macro. Notably, AES-GCM tag authentication
89 // failures don't produce any error code, though they should probably be BAD_DECRYPT.
90 JSG_REQUIRE(ec == 0 || ec == ERR_PACK(ERR_LIB_CIPHER, CIPHER_R_BAD_DECRYPT) ||
91 ec == ERR_PACK(ERR_LIB_CIPHER, CIPHER_R_WRONG_FINAL_BLOCK_LENGTH),
92 InternalDOMOperationError, "Unexpected issue decrypting", internalDescribeOpensslErrors());
93 
94 // Consume the error since it's one we were expecting.
95 clearErrorOnReturn.consumeError();
96 
97 // Otherwise, tell the script author they gave us garbage.
98 JSG_FAIL_REQUIRE(DOMOperationError,
99 "Decryption failed. This could be due "
100 "to a ciphertext authentication failure, bad padding, incorrect CryptoKey, or another "
101 "algorithm-specific reason. Input length was ",
102 inputLength, ", output length expected to be ", outputLength, " for ", algorithm);
103}
104 
105// NOTE: The OpenSSL calls to implement AES-GCM and AES-CBC are quite similar. If you update one
106// algorithm's encrypt() or decrypt() implementation, it'd be worth reviewing the other
107// algorithm's implementation as well.
108 
109// The base key is used to avoid repeating the JWK export logic. It also happens to simplify the
110// concrete implementations to only define encrypt/decrypt.
111class AesKeyBase: public CryptoKey::Impl {
112 public:
113 explicit AesKeyBase(kj::Array<kj::byte> keyData,
114 CryptoKey::AesKeyAlgorithm keyAlgorithm,
115 bool extractable,
116 CryptoKeyUsageSet usages)
117 : CryptoKey::Impl(extractable, usages),
118 keyData(kj::mv(keyData)),
119 keyAlgorithm(kj::mv(keyAlgorithm)) {}
120 
121 protected:
122 kj::StringPtr getAlgorithmName() const override final {
123 // AesKeyAlgorithm is constructed from normalizedName which points into the static constant
124 // defined in crypto.c++ for lookup.
125 return keyAlgorithm.name;
126 }
127 
128 bool equals(const CryptoKey::Impl& other) const override final {
129 return this == &other || (other.getType() == "secret"_kj && other.equals(keyData));
130 }
131 
132 bool equals(const kj::Array<kj::byte>& other) const override final {
133 return keyData.size() == other.size() &&
134 CRYPTO_memcmp(keyData.begin(), other.begin(), keyData.size()) == 0;
135 }
136 
137 kj::StringPtr jsgGetMemoryName() const override {
138 return "AesKeyBase"_kjc;
139 }
140 size_t jsgGetMemorySelfSize() const override {
141 return sizeof(AesKeyBase);
142 }
143 void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override {
144 tracker.trackFieldWithSize("keyData", keyData.size());
145 tracker.trackField("keyAlgorithm", keyAlgorithm);
146 }
147 
148 private:
149 CryptoKey::AlgorithmVariant getAlgorithm(jsg::Lock& js) const override final {
150 return keyAlgorithm;
151 }
152 
153 SubtleCrypto::ExportKeyData exportKey(jsg::Lock& js, kj::StringPtr format) const override final {
154 JSG_REQUIRE(format == "raw" || format == "jwk", DOMNotSupportedError, getAlgorithmName(),
155 " key only supports exporting \"raw\" & \"jwk\", not \"", format, "\".");
156 
157 if (format == "jwk") {
158 auto lengthInBytes = keyData.size();
159 KJ_ASSERT(lengthInBytes == 16 || lengthInBytes == 24 || lengthInBytes == 32);
160 
161 auto aesMode = keyAlgorithm.name.slice(4);
162 
163#ifdef KJ_DEBUG
164 static constexpr auto expectedModes = {"GCM", "KW", "CTR", "CBC"};
165 KJ_DASSERT(
166 expectedModes.end() != std::find(expectedModes.begin(), expectedModes.end(), aesMode));
167#endif
168 
169 SubtleCrypto::JsonWebKey jwk;
170 jwk.kty = kj::str("oct");
171 jwk.k = fastEncodeBase64Url(keyData);
172 jwk.alg = kj::str("A", lengthInBytes * 8, aesMode);
173 jwk.key_ops = getUsages().map([](auto usage) { return kj::str(usage.name()); });
174 // I don't know why the spec says:
175 // Set the ext attribute of jwk to equal the [[extractable]] internal slot of key.
176 // Earlier in the normative part of the spec it says:
177 // 6. If the [[extractable]] internal slot of key is false, then throw an InvalidAccessError.
178 // 7. Let result be the result of performing the export key operation specified by the
179 // [[algorithm]] internal slot of key using key and format.
180 // So there's not really any other value that `ext` can have here since this code is the
181 // implementation of step 7 (see SubtleCrypto::exportKey where you can confirm it is
182 // enforcing step 6).
183 jwk.ext = true;
184 
185 return jwk;
186 }
187 
188 // Every export should be a separate copy.
189 return jsg::JsArrayBuffer::create(js, keyData).addRef(js);
190 }
191 
192 protected:
193 ZeroOnFree keyData;
194 CryptoKey::AesKeyAlgorithm keyAlgorithm;
195};
196 
197class AesGcmKey final: public AesKeyBase {
198 public:
199 explicit AesGcmKey(kj::Array<kj::byte> keyData,
200 CryptoKey::AesKeyAlgorithm keyAlgorithm,
201 bool extractable,
202 CryptoKeyUsageSet usages)
203 : AesKeyBase(kj::mv(keyData), kj::mv(keyAlgorithm), extractable, usages) {}
204 
205 private:
206 jsg::JsArrayBuffer encrypt(jsg::Lock& js,
207 SubtleCrypto::EncryptAlgorithm&& algorithm,
208 kj::ArrayPtr<const kj::byte> plainText) const override {
209 auto iv = JSG_REQUIRE_NONNULL(algorithm.iv, TypeError, "Missing field \"iv\" in \"algorithm\".")
210 .getHandle(js);
211 JSG_REQUIRE(iv.size() != 0, DOMOperationError, "AES-GCM IV must not be empty.");
212 
213 kj::ArrayPtr<kj::byte> empty = nullptr;
214 auto additionalData = ([&] {
215 KJ_IF_SOME(sourceRef, algorithm.additionalData) {
216 auto source = sourceRef.getHandle(js);
217 return source.asArrayPtr();
218 } else {
219 return empty;
220 }
221 })();
222 
223 // The magic number below came from here:
224 // https://w3c.github.io/webcrypto/Overview.html#aes-gcm-operations
225 JSG_REQUIRE(plainText.size() <= ((UINT64_C(1) << 39) - 256), DOMOperationError,
226 "AES-GCM can only encrypt up to 2^39 - 256 bytes of plaintext at a time, but requested ",
227 plainText.size(), " bytes.");
228 
229 int tagLength = algorithm.tagLength.orDefault(128);
230 validateAesGcmTagLength(tagLength);
231 
232 auto cipherCtx = kj::disposeWith<EVP_CIPHER_CTX_free>(EVP_CIPHER_CTX_new());
233 KJ_ASSERT(cipherCtx.get() != nullptr);
234 
235 auto type = lookupAesGcmType(keyData.size() * 8);
236 
237 // Set up the cipher context with the initialization vector. We pass nullptrs for the key data
238 // and initialization vector because we may need to override the default IV length.
239 OSSLCALL(EVP_EncryptInit_ex(cipherCtx.get(), type, nullptr, nullptr, nullptr));
240 OSSLCALL(EVP_CIPHER_CTX_ctrl(cipherCtx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr));
241 OSSLCALL(EVP_EncryptInit_ex(
242 cipherCtx.get(), nullptr, nullptr, keyData.begin(), iv.asArrayPtr().begin()));
243 
244 if (additionalData.size() > 0) {
245 // Run the engine with the additional data, which will presumably be transmitted alongside the
246 // cipher text in plain text. I noticed that if I call EncryptUpdate with 0-length AAD here,
247 // the subsequent call to EncryptUpdate will fail, thus the if-check.
248 int dummy;
249 OSSLCALL(EVP_EncryptUpdate(
250 cipherCtx.get(), nullptr, &dummy, additionalData.begin(), additionalData.size()));
251 }
252 
253 // We make two cipher calls: EVP_EncryptUpdate() and EVP_EncryptFinal_ex(). AES-GCM behaves like
254 // a stream cipher in that it does not add padding and can process partial blocks, meaning that
255 // we know the exact ciphertext size in advance.
256 auto tagByteSize = tagLength / 8;
257 auto cipherText = jsg::JsArrayBuffer::create(js, plainText.size() + tagByteSize);
258 
259 // Perform the actual encryption.
260 
261 int cipherSize = 0;
262 OSSLCALL(EVP_EncryptUpdate(cipherCtx.get(), cipherText.asArrayPtr().begin(), &cipherSize,
263 plainText.begin(), plainText.size()));
264 KJ_ASSERT(cipherSize == plainText.size(), "EVP_EncryptUpdate should encrypt all at once");
265 
266 int finalCipherSize = 0;
267 OSSLCALL(EVP_EncryptFinal_ex(
268 cipherCtx.get(), cipherText.asArrayPtr().begin() + cipherSize, &finalCipherSize));
269 KJ_ASSERT(finalCipherSize == 0, "EVP_EncryptFinal_ex should not output any data");
270 
271 // Concatenate the tag onto the cipher text.
272 KJ_ASSERT(cipherSize + tagByteSize == cipherText.size(), "imminent buffer overrun");
273 OSSLCALL(EVP_CIPHER_CTX_ctrl(cipherCtx.get(), EVP_CTRL_GCM_GET_TAG, tagByteSize,
274 cipherText.asArrayPtr().begin() + cipherSize));
275 cipherSize += tagByteSize;
276 KJ_ASSERT(cipherSize == cipherText.size(), "buffer overrun");
277 
278 return cipherText;
279 }
280 
281 jsg::JsArrayBuffer decrypt(jsg::Lock& js,
282 SubtleCrypto::EncryptAlgorithm&& algorithm,
283 kj::ArrayPtr<const kj::byte> cipherText) const override {
284 auto iv = JSG_REQUIRE_NONNULL(algorithm.iv, TypeError, "Missing field \"iv\" in \"algorithm\".")
285 .getHandle(js);
286 JSG_REQUIRE(iv.size() != 0, DOMOperationError, "AES-GCM IV must not be empty.");
287 
288 int tagLength = algorithm.tagLength.orDefault(128);
289 validateAesGcmTagLength(tagLength);
290 
291 JSG_REQUIRE(cipherText.size() >= tagLength / 8, DOMOperationError, "Ciphertext length of ",
292 cipherText.size() * 8,
293 " bits must be greater than or equal to "
294 "the size of the AES-GCM tag length of ",
295 tagLength, " bits.");
296 
297 kj::ArrayPtr<kj::byte> empty = nullptr;
298 auto additionalData = ([&] {
299 KJ_IF_SOME(sourceRef, algorithm.additionalData) {
300 auto source = sourceRef.getHandle(js);
301 return source.asArrayPtr();
302 }
303 return empty;
304 })();
305 
306 auto cipherCtx = kj::disposeWith<EVP_CIPHER_CTX_free>(EVP_CIPHER_CTX_new());
307 KJ_ASSERT(cipherCtx.get() != nullptr);
308 
309 auto type = lookupAesGcmType(keyData.size() * 8);
310 
311 OSSLCALL(EVP_DecryptInit_ex(cipherCtx.get(), type, nullptr, nullptr, nullptr));
312 OSSLCALL(EVP_CIPHER_CTX_ctrl(cipherCtx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr));
313 OSSLCALL(EVP_DecryptInit_ex(
314 cipherCtx.get(), nullptr, nullptr, keyData.begin(), iv.asArrayPtr().begin()));
315 
316 int plainSize = 0;
317 
318 if (additionalData.size() > 0) {
319 OSSLCALL(EVP_DecryptUpdate(
320 cipherCtx.get(), nullptr, &plainSize, additionalData.begin(), additionalData.size()));
321 plainSize = 0;
322 }
323 
324 auto actualCipherText = cipherText.first(cipherText.size() - tagLength / 8);
325 auto tagText = cipherText.slice(actualCipherText.size(), cipherText.size());
326 
327 auto plainText = jsg::JsArrayBuffer::create(js, actualCipherText.size());
328 
329 // Perform the actual decryption.
330 OSSLCALL(EVP_DecryptUpdate(cipherCtx.get(), plainText.asArrayPtr().begin(), &plainSize,
331 actualCipherText.begin(), actualCipherText.size()));
332 KJ_ASSERT(plainSize == plainText.size());
333 
334 // NOTE: We const_cast tagText here. EVP_CIPHER_CTX_ctrl() is used to set various
335 // cipher-specific parameters, not just the GCM tag. Because of this, it takes its pointer
336 // parameter as a void*, thus the const_cast. This is safe because tagText points to a
337 // BufferSource allocated on V8's heap, and we know that OpenSSL does not modify the tag. (If
338 // it did, the W3C crypto tests would fail.)
339 //
340 // This little hack seems like a lesser evil than accepting the plaintext as mutable in every
341 // decrypt implementation function interface.
342 OSSLCALL(EVP_CIPHER_CTX_ctrl(cipherCtx.get(), EVP_CTRL_GCM_SET_TAG, tagLength / 8,
343 const_cast<kj::byte*>(tagText.begin())));
344 
345 plainSize += decryptFinalHelper(getAlgorithmName(), actualCipherText.size(), plainSize,
346 cipherCtx.get(), plainText.asArrayPtr().begin() + plainSize);
347 KJ_ASSERT(plainSize == plainText.size());
348 
349 return plainText;
350 }
351};
352 
353class AesCbcKey final: public AesKeyBase {
354 public:
355 explicit AesCbcKey(kj::Array<kj::byte> keyData,
356 CryptoKey::AesKeyAlgorithm keyAlgorithm,
357 bool extractable,
358 CryptoKeyUsageSet usages)
359 : AesKeyBase(kj::mv(keyData), kj::mv(keyAlgorithm), extractable, usages) {}
360 
361 private:
362 jsg::JsArrayBuffer encrypt(jsg::Lock& js,
363 SubtleCrypto::EncryptAlgorithm&& algorithm,
364 kj::ArrayPtr<const kj::byte> plainText) const override {
365 auto iv = JSG_REQUIRE_NONNULL(algorithm.iv, TypeError, "Missing field \"iv\" in \"algorithm\".")
366 .getHandle(js);
367 
368 JSG_REQUIRE(iv.size() == 16, DOMOperationError, "AES-CBC IV must be 16 bytes long (provided ",
369 iv.size(), " bytes).");
370 
371 auto cipherCtx = kj::disposeWith<EVP_CIPHER_CTX_free>(EVP_CIPHER_CTX_new());
372 KJ_ASSERT(cipherCtx.get() != nullptr);
373 auto type = lookupAesCbcType(keyData.size() * 8);
374 
375 // Set up the cipher context with the initialization vector.
376 OSSLCALL(EVP_EncryptInit_ex(
377 cipherCtx.get(), type, nullptr, keyData.begin(), iv.asArrayPtr().begin()));
378 
379 auto blockSize = EVP_CIPHER_CTX_block_size(cipherCtx.get());
380 size_t paddingSize = blockSize - (plainText.size() % blockSize);
381 auto cipherText = jsg::JsArrayBuffer::create(js, plainText.size() + paddingSize);
382 
383 // Perform the actual encryption.
384 //
385 // Note: We don't worry about PKCS padding (see RFC2315 section 10.3 step 2) because BoringSSL
386 // takes care of it for us by default in EVP_EncryptFinal_ex().
387 
388 int cipherSize = 0;
389 OSSLCALL(EVP_EncryptUpdate(cipherCtx.get(), cipherText.asArrayPtr().begin(), &cipherSize,
390 plainText.begin(), plainText.size()));
391 KJ_ASSERT(cipherSize <= cipherText.size(), "buffer overrun");
392 
393 KJ_ASSERT(cipherSize + blockSize <= cipherText.size(), "imminent buffer overrun");
394 int finalCipherSize = 0;
395 OSSLCALL(EVP_EncryptFinal_ex(
396 cipherCtx.get(), cipherText.asArrayPtr().begin() + cipherSize, &finalCipherSize));
397 cipherSize += finalCipherSize;
398 KJ_ASSERT(cipherSize == cipherText.size(), "buffer overrun");
399 
400 return cipherText;
401 }
402 
403 jsg::JsArrayBuffer decrypt(jsg::Lock& js,
404 SubtleCrypto::EncryptAlgorithm&& algorithm,
405 kj::ArrayPtr<const kj::byte> cipherText) const override {
406 auto iv = JSG_REQUIRE_NONNULL(algorithm.iv, TypeError, "Missing field \"iv\" in \"algorithm\".")
407 .getHandle(js);
408 
409 JSG_REQUIRE(iv.size() == 16, DOMOperationError, "AES-CBC IV must be 16 bytes long (provided ",
410 iv.size(), ").");
411 
412 auto cipherCtx = kj::disposeWith<EVP_CIPHER_CTX_free>(EVP_CIPHER_CTX_new());
413 KJ_ASSERT(cipherCtx.get() != nullptr);
414 
415 auto type = lookupAesCbcType(keyData.size() * 8);
416 
417 // Set up the cipher context with the initialization vector.
418 OSSLCALL(EVP_DecryptInit_ex(
419 cipherCtx.get(), type, nullptr, keyData.begin(), iv.asArrayPtr().begin()));
420 
421 int plainSize = 0;
422 auto blockSize = EVP_CIPHER_CTX_block_size(cipherCtx.get());
423 
424 KJ_STACK_ARRAY(
425 kj::byte, plainText, cipherText.size() + ((blockSize > 1) ? blockSize : 0), 1024, 4096);
426 
427 // Perform the actual decryption.
428 OSSLCALL(EVP_DecryptUpdate(
429 cipherCtx.get(), plainText.begin(), &plainSize, cipherText.begin(), cipherText.size()));
430 KJ_ASSERT(plainSize + ((blockSize > 1) ? blockSize : 0) <= plainText.size());
431 
432 plainSize += decryptFinalHelper(getAlgorithmName(), cipherText.size(), plainSize,
433 cipherCtx.get(), plainText.begin() + plainSize);
434 KJ_ASSERT(plainSize <= plainText.size());
435 
436 // Copy is necessary to support v8:Sandbox where all ArrayBuffers have to be
437 // allocated from within the sandbox.
438 return jsg::JsArrayBuffer::create(js, plainText.first(plainSize));
439 }
440};
441 
442class AesCtrKey final: public AesKeyBase {
443 static constexpr size_t expectedCounterByteSize = 16;
444 
445 public:
446 explicit AesCtrKey(kj::Array<kj::byte> keyData,
447 CryptoKey::AesKeyAlgorithm keyAlgorithm,
448 bool extractable,
449 CryptoKeyUsageSet usages)
450 : AesKeyBase(kj::mv(keyData), kj::mv(keyAlgorithm), extractable, usages) {}
451 
452 jsg::JsArrayBuffer encrypt(jsg::Lock& js,
453 SubtleCrypto::EncryptAlgorithm&& algorithm,
454 kj::ArrayPtr<const kj::byte> plainText) const override {
455 return encryptOrDecrypt(js, kj::mv(algorithm), plainText);
456 }
457 
458 jsg::JsArrayBuffer decrypt(jsg::Lock& js,
459 SubtleCrypto::EncryptAlgorithm&& algorithm,
460 kj::ArrayPtr<const kj::byte> cipherText) const override {
461 return encryptOrDecrypt(js, kj::mv(algorithm), cipherText);
462 }
463 
464 protected:
465 static const EVP_CIPHER& lookupAesType(size_t keyLengthBytes) {
466 switch (keyLengthBytes) {
467 case 16:
468 return *EVP_aes_128_ctr();
469 // NOTE: FWIW Chrome intentionally doesn't support 192 (http://crbug.com/533699) & at one
470 // point in removal of the 192 variant was scheduled for removal from BoringSSL. However, we
471 // do support it for completeness (as does Firefox).
472 case 24:
473 return *EVP_aes_192_ctr();
474 case 32:
475 return *EVP_aes_256_ctr();
476 }
477 KJ_FAIL_ASSERT("CryptoKey has invalid data length");
478 }
479 
480 jsg::JsArrayBuffer encryptOrDecrypt(jsg::Lock& js,
481 SubtleCrypto::EncryptAlgorithm&& algorithm,
482 kj::ArrayPtr<const kj::byte> data) const {
483 auto counter = JSG_REQUIRE_NONNULL(
484 algorithm.counter, TypeError, "Missing \"counter\" member in \"algorithm\".")
485 .getHandle(js);
486 JSG_REQUIRE(counter.size() == expectedCounterByteSize, DOMOperationError,
487 "Counter must have length of 16 bytes (provided ", counter.size(), ").");
488 
489 auto& counterBitLength = JSG_REQUIRE_NONNULL(
490 algorithm.length, TypeError, "Missing \"length\" member in \"algorithm\".");
491 
492 // Web IDL defines an octet as [0, 255] which explains why the spec here only calls out != 0 and
493 // <= 128, which implies the intended range must be [1, 128] which is what we enforce here.
494 // However, we check > 0 instead of != 0 because we don't enforce the bounds of an octet when
495 // converting from JS. If we were to ever add support for annotations into JSG (specifically
496 // EnforceRange), then we'd have enforcement way before this that counterBitLength is in the
497 // [0, 255] range:
498 // * https://heycam.github.io/webidl/#EnforceRange,
499 // * https://heycam.github.io/webidl/#es-octet
500 // * https://heycam.github.io/webidl/#abstract-opdef-converttoint
501 JSG_REQUIRE(counterBitLength > 0 && counterBitLength <= 128, DOMOperationError,
502 "Invalid counter of ", counterBitLength, " bits length provided.");
503 
504 const auto& cipher = lookupAesType(keyData.size());
505 
506 // The output of AES-CTR is the same size as the input.
507 auto result = jsg::JsArrayBuffer::create(js, data.size());
508 
509 auto numCounterValues = newBignum();
510 JSG_REQUIRE(BN_lshift(numCounterValues.get(), BN_value_one(), counterBitLength),
511 InternalDOMOperationError, "Error doing ", getAlgorithmName(), " encrypt/decrypt",
512 internalDescribeOpensslErrors());
513 
514 auto currentCounter = getCounter(counter.asArrayPtr(), counterBitLength);
515 
516 // Now figure out how many AES blocks we'll process/how many times to increment the counter.
517 auto numOutputBlocks = newBignum();
518 JSG_REQUIRE(BN_set_word(numOutputBlocks.get(),
519 integerCeilDivision(result.size(), static_cast<size_t>(AES_BLOCK_SIZE))),
520 InternalDOMOperationError, "Error doing ", getAlgorithmName(), " encrypt/decrypt",
521 internalDescribeOpensslErrors());
522 
523 JSG_REQUIRE(BN_cmp(numOutputBlocks.get(), numCounterValues.get()) <= 0, DOMOperationError,
524 "Counter block values will repeat", tryDescribeOpensslErrors());
525 
526 auto numBlocksUntilReset = newBignum();
527 // The number of blocks that can be encrypted without overflowing the counter. Subsequent
528 // blocks will need to reset the counter back to 0. BN_sub's signature is (result, a, b) and
529 // evaluates result = a - b. BN_sub documentation says an error happens on allocation failure
530 // but I can't find any evidence there's any such allocation & the errors seem to be a result
531 // of internal errors.
532 JSG_REQUIRE(BN_sub(numBlocksUntilReset.get(), numCounterValues.get(), currentCounter.get()),
533 InternalDOMOperationError, "Error doing ", getAlgorithmName(), " encrypt/decrypt",
534 internalDescribeOpensslErrors());
535 
536 if (BN_cmp(numBlocksUntilReset.get(), numOutputBlocks.get()) >= 0) {
537 // If the counter doesn't need any wrapping, can evaluate this as a single call.
538 process(&cipher, data, counter.asArrayPtr(), result.asArrayPtr());
539 return result;
540 }
541 
542 // Need this to be done in 2 parts using the current counter block and then resetting the
543 // counter portion of the block back to zero.
544 auto inputSizePart1 = BN_get_word(numBlocksUntilReset.get()) * AES_BLOCK_SIZE;
545 
546 process(&cipher, data.first(inputSizePart1), counter.asArrayPtr(), result.asArrayPtr());
547 
548 // Zero the counter bits of the block in a copy of the input counter.
549 kj::Array<kj::byte> zeroed_counter = counter.copy();
550 {
551 KJ_DASSERT(counterBitLength / 8 <= expectedCounterByteSize);
552 
553 auto remainder = counterBitLength % 8;
554 auto idx = expectedCounterByteSize - counterBitLength / 8;
555 zeroed_counter.slice(idx).fill(0);
556 if (remainder) {
557 zeroed_counter[idx - 1] &= 0xFF << remainder;
558 }
559 }
560 
561 process(&cipher, data.slice(inputSizePart1, data.size()), zeroed_counter,
562 result.asArrayPtr().slice(inputSizePart1, result.size()));
563 
564 return result;
565 }
566 
567 private:
568 kj::Own<BIGNUM> getCounter(
569 kj::ArrayPtr<kj::byte> counterBlock, const unsigned counterBitLength) const {
570 // See GetCounter from https://chromium.googlesource.com/chromium/src/+/refs/tags/91.0.4458.2/components/webcrypto/algorithms/aes_ctr.cc#86
571 // The counter is the rightmost "counterBitLength" of the block as a big-endian number.
572 KJ_DASSERT(counterBlock.size() == expectedCounterByteSize);
573 
574 auto remainderBits = counterBitLength % 8;
575 if (remainderBits == 0) {
576 // Multiple of 8 bits, then can pass the remainder to BN_bin2bn (binary to bignum).
577 auto byteLength = counterBitLength / 8;
578 auto remainingCounter =
579 counterBlock.slice(expectedCounterByteSize - byteLength, counterBlock.size());
580 
581 return JSG_REQUIRE_NONNULL(toBignum(remainingCounter.asBytes()), InternalDOMOperationError,
582 "Error doing ", getAlgorithmName(), " encrypt/decrypt", internalDescribeOpensslErrors());
583 }
584 
585 // Convert the counter but zero out the topmost bits so that we can convert to bignum from a
586 // byte stream. Chromium creates a copy here but that's because they only have a const only view
587 // of the data but in our WebCrypto implementation we have a non-const view of the underlying
588 // counter buffer.
589 auto byteLength = integerCeilDivision(counterBitLength, 8u);
590 KJ_DASSERT(byteLength > 0, counterBitLength, remainderBits);
591 KJ_DASSERT(byteLength <= expectedCounterByteSize, counterBitLength, counterBlock.size());
592 
593 auto counterToProcess =
594 counterBlock.slice(expectedCounterByteSize - byteLength, counterBlock.size());
595 auto previous = counterToProcess[0];
596 counterToProcess[0] &= ~(0xFF << remainderBits);
597 KJ_DEFER(counterToProcess[0] = previous);
598 // We temporarily modified the counter to construct the BIGNUM, this undoes it to restore the
599 // input counter.
600 
601 return JSG_REQUIRE_NONNULL(toBignum(counterToProcess), InternalDOMOperationError,
602 "Error doing ", getAlgorithmName(), " encrypt/decrypt", internalDescribeOpensslErrors());
603 }
604 
605 void process(const EVP_CIPHER* cipher,
606 kj::ArrayPtr<const kj::byte> input,
607 kj::ArrayPtr<kj::byte> counter,
608 kj::ArrayPtr<kj::byte> output) const {
609 // Workers are limited to 128MB so this isn't actually a realistic concern, but sanity check.
610 JSG_REQUIRE(input.size() < INT_MAX, DOMOperationError, "Input is too large to encrypt.");
611 
612 auto cipherContext = kj::disposeWith<EVP_CIPHER_CTX_free>(EVP_CIPHER_CTX_new());
613 KJ_ASSERT(cipherContext.get() != nullptr);
614 
615 // For CTR, it really does not matter whether we are encrypting or decrypting, so set enc to 0.
616 JSG_REQUIRE(EVP_CipherInit_ex(cipherContext.get(), cipher, nullptr,
617 keyData.asPtr().asBytes().begin(), counter.asBytes().begin(), 0),
618 InternalDOMOperationError, "Error doing ", getAlgorithmName(), " encrypt/decrypt",
619 internalDescribeOpensslErrors());
620 
621 int outputLength = 0;
622 JSG_REQUIRE(EVP_CipherUpdate(cipherContext.get(), output.begin(), &outputLength,
623 input.asBytes().begin(), input.size()),
624 InternalDOMOperationError, "Error doing ", getAlgorithmName(), " encrypt/decrypt",
625 internalDescribeOpensslErrors());
626 
627 KJ_ASSERT(outputLength >= 0 && outputLength <= output.size(), outputLength, output.size());
628 
629 int finalOutputChunkLength = 0;
630 auto finalizationBuffer = output.slice(outputLength, output.size()).asBytes().begin();
631 JSG_REQUIRE(
632 EVP_CipherFinal_ex(cipherContext.get(), finalizationBuffer, &finalOutputChunkLength),
633 InternalDOMOperationError, "Error doing ", getAlgorithmName(), " encrypt/decrypt",
634 internalDescribeOpensslErrors());
635 
636 KJ_ASSERT(finalOutputChunkLength >= 0 && finalOutputChunkLength <= output.size(),
637 finalOutputChunkLength, output.size());
638 
639 JSG_REQUIRE(static_cast<size_t>(outputLength) + static_cast<size_t>(finalOutputChunkLength) ==
640 input.size(),
641 InternalDOMOperationError, "Error doing ", getAlgorithmName(), " encrypt/decrypt.");
642 }
643};
644 
645class AesKwKey final: public AesKeyBase {
646 public:
647 explicit AesKwKey(kj::Array<kj::byte> keyData,
648 CryptoKey::AesKeyAlgorithm keyAlgorithm,
649 bool extractable,
650 CryptoKeyUsageSet usages)
651 : AesKeyBase(kj::mv(keyData), kj::mv(keyAlgorithm), extractable, usages) {}
652 
653 jsg::JsArrayBuffer wrapKey(jsg::Lock& js,
654 SubtleCrypto::EncryptAlgorithm&& algorithm,
655 kj::ArrayPtr<const kj::byte> unwrappedKey) const override {
656 // Resources used to implement this:
657 // https://www.ietf.org/rfc/rfc3394.txt
658 // https://chromium.googlesource.com/chromium/src/+/refs/tags/91.0.4458.2/components/webcrypto/algorithms/aes_kw.cc
659 
660 JSG_REQUIRE((unwrappedKey.size() % 8) == 0, DOMOperationError,
661 "Unwrapped key bit length must be a multiple of 64 bits but unwrapped key has a length of ",
662 unwrappedKey.size() * 8, " bits.");
663 
664 JSG_REQUIRE(unwrappedKey.size() >= 16 && unwrappedKey.size() <= SIZE_MAX - 8, DOMOperationError,
665 "Unwrapped key has length ", unwrappedKey.size(),
666 " bytes but it should be greater than or "
667 "equal to 16 and less than or equal to ",
668 SIZE_MAX - 8);
669 
670 auto wrapped = jsg::JsArrayBuffer::create(js, unwrappedKey.size() + 8);
671 // Wrapping adds 8 bytes of overhead for storing the IV which we check on decryption.
672 
673 AES_KEY aesKey;
674 JSG_REQUIRE(0 == AES_set_encrypt_key(keyData.begin(), keyData.size() * 8, &aesKey),
675 InternalDOMOperationError, "Error doing ", getAlgorithmName(), " key wrapping",
676 internalDescribeOpensslErrors());
677 
678 JSG_REQUIRE(wrapped.size() ==
679 AES_wrap_key(&aesKey, nullptr, wrapped.asArrayPtr().begin(), unwrappedKey.begin(),
680 unwrappedKey.size()),
681 DOMOperationError, getAlgorithmName(), " key wrapping failed", tryDescribeOpensslErrors());
682 
683 return wrapped;
684 }
685 
686 jsg::JsArrayBuffer unwrapKey(jsg::Lock& js,
687 SubtleCrypto::EncryptAlgorithm&& algorithm,
688 kj::ArrayPtr<const kj::byte> wrappedKey) const override {
689 // Resources used to implement this:
690 // https://www.ietf.org/rfc/rfc3394.txt
691 // https://chromium.googlesource.com/chromium/src/+/refs/tags/91.0.4458.2/components/webcrypto/algorithms/aes_kw.cc
692 
693 JSG_REQUIRE((wrappedKey.size() % 8) == 0, DOMOperationError,
694 "Provided a wrapped key to unwrap that is ", wrappedKey.size() * 8,
695 " bits which isn't a multiple of 64 bits.");
696 
697 JSG_REQUIRE(wrappedKey.size() >= 24, DOMOperationError,
698 "Provided a wrapped key to unwrap this is ", wrappedKey.size() * 8,
699 " bits that is less than the minimal length of 192 bits.");
700 
701 auto unwrapped = jsg::JsArrayBuffer::create(js, wrappedKey.size() - 8);
702 
703 AES_KEY aesKey;
704 JSG_REQUIRE(0 == AES_set_decrypt_key(keyData.begin(), keyData.size() * 8, &aesKey),
705 InternalDOMOperationError, "Error doing ", getAlgorithmName(), " key unwrapping",
706 internalDescribeOpensslErrors());
707 
708 // null for the IV value here will tell OpenSSL to validate using the default IV from RFC3394.
709 // https://github.com/openssl/openssl/blob/13a574d8bb2523181f8150de49bc041c9841f59d/crypto/modes/wrap128.c
710 JSG_REQUIRE(unwrapped.size() ==
711 AES_unwrap_key(&aesKey, nullptr, unwrapped.asArrayPtr().begin(), wrappedKey.begin(),
712 wrappedKey.size()),
713 DOMOperationError, getAlgorithmName(), " key unwrapping failed",
714 tryDescribeOpensslErrors());
715 
716 return unwrapped;
717 }
718};
719 
720CryptoKeyUsageSet validateAesUsages(CryptoKeyUsageSet::Context ctx,
721 kj::StringPtr normalizedName,
722 kj::ArrayPtr<const kj::String> keyUsages) {
723 // AES-CTR, AES-CBC, AES-GCM, and AES-KW all share the same logic for operations, with the only
724 // difference being the valid usages.
725 CryptoKeyUsageSet validUsages = CryptoKeyUsageSet::wrapKey() | CryptoKeyUsageSet::unwrapKey();
726 if (normalizedName != "AES-KW") {
727 validUsages |= CryptoKeyUsageSet::encrypt() | CryptoKeyUsageSet::decrypt();
728 }
729 return CryptoKeyUsageSet::validate(normalizedName, ctx, keyUsages, validUsages);
730}
731 
732} // namespace
733 
734kj::OneOf<jsg::Ref<CryptoKey>, CryptoKeyPair> CryptoKey::Impl::generateAes(jsg::Lock& js,
735 kj::StringPtr normalizedName,
736 SubtleCrypto::GenerateKeyAlgorithm&& algorithm,
737 bool extractable,
738 kj::ArrayPtr<const kj::String> keyUsages) {
739 CryptoKeyUsageSet usages =
740 validateAesUsages(CryptoKeyUsageSet::Context::generate, normalizedName, keyUsages);
741 
742 auto length = JSG_REQUIRE_NONNULL(
743 algorithm.length, TypeError, "Missing field \"length\" in \"algorithm\".");
744 
745 switch (length) {
746 case 128:
747 case 192:
748 case 256:
749 break;
750 default:
751 JSG_FAIL_REQUIRE(DOMOperationError,
752 "Generated AES key length must be 128, 192, or 256 bits but requested ", length, ".");
753 }
754 
755 auto keyDataArray = kj::heapArray<kj::byte>(length / 8);
756 IoContext::current().getEntropySource().generate(keyDataArray);
757 
758 auto keyAlgorithm = CryptoKey::AesKeyAlgorithm{normalizedName, static_cast<uint16_t>(length)};
759 
760 kj::Own<CryptoKey::Impl> keyImpl;
761 
762 if (normalizedName == "AES-GCM") {
763 keyImpl = kj::heap<AesGcmKey>(kj::mv(keyDataArray), kj::mv(keyAlgorithm), extractable, usages);
764 } else if (normalizedName == "AES-CBC") {
765 keyImpl = kj::heap<AesCbcKey>(kj::mv(keyDataArray), kj::mv(keyAlgorithm), extractable, usages);
766 } else if (normalizedName == "AES-CTR") {
767 keyImpl = kj::heap<AesCtrKey>(kj::mv(keyDataArray), kj::mv(keyAlgorithm), extractable, usages);
768 } else if (normalizedName == "AES-KW") {
769 keyImpl = kj::heap<AesKwKey>(kj::mv(keyDataArray), kj::mv(keyAlgorithm), extractable, usages);
770 } else {
771 JSG_FAIL_REQUIRE(DOMNotSupportedError, normalizedName, " key generation not supported.");
772 }
773 
774 return js.alloc<CryptoKey>(kj::mv(keyImpl));
775}
776 
777kj::Own<CryptoKey::Impl> CryptoKey::Impl::importAes(jsg::Lock& js,
778 kj::StringPtr normalizedName,
779 kj::StringPtr format,
780 SubtleCrypto::ImportKeyData keyData,
781 SubtleCrypto::ImportKeyAlgorithm&& algorithm,
782 bool extractable,
783 kj::ArrayPtr<const kj::String> keyUsages) {
784 CryptoKeyUsageSet usages =
785 validateAesUsages(CryptoKeyUsageSet::Context::importSecret, normalizedName, keyUsages);
786 
787 kj::Array<kj::byte> keyDataArray;
788 
789 if (format == "raw") {
790 // NOTE: Checked in SubtleCrypto::importKey().
791 keyDataArray = kj::mv(keyData.get<kj::Array<kj::byte>>());
792 switch (keyDataArray.size() * 8) {
793 case 128:
794 case 192:
795 case 256:
796 break;
797 default:
798 JSG_FAIL_REQUIRE(DOMDataError,
799 "Imported AES key length must be 128, 192, or 256 bits but provided ",
800 keyDataArray.size() * 8, ".");
801 }
802 } else if (format == "jwk") {
803 auto aesMode = normalizedName.slice(4);
804 
805 auto& keyDataJwk = keyData.get<SubtleCrypto::JsonWebKey>();
806 JSG_REQUIRE(keyDataJwk.kty == "oct", DOMDataError,
807 "Symmetric \"jwk\" key import requires a JSON Web Key with Key Type parameter "
808 "\"kty\" equal to \"oct\" (encountered \"",
809 keyDataJwk.kty, "\").");
810 // https://www.rfc-editor.org/rfc/rfc7518.txt Section 6.1
811 keyDataArray = UNWRAP_JWK_BIGNUM(kj::mv(keyDataJwk.k), DOMDataError,
812 "Symmetric \"jwk\" key import requires a base64Url encoding of the key.");
813 
814 switch (keyDataArray.size() * 8) {
815 case 128:
816 case 192:
817 case 256:
818 KJ_IF_SOME(alg, keyDataJwk.alg) {
819 auto expectedAlg = kj::str("A", keyDataArray.size() * 8, aesMode);
820 JSG_REQUIRE(alg == expectedAlg, DOMDataError,
821 "Symmetric \"jwk\" key contains invalid \"alg\" value \"", alg, "\", expected \"",
822 expectedAlg, "\".");
823 }
824 break;
825 default:
826 JSG_FAIL_REQUIRE(DOMDataError,
827 "Imported AES key length must be 128, 192, or 256 bits but provided ",
828 keyDataArray.size() * 8, ".");
829 }
830 
831 if (keyUsages.size() != 0) {
832 KJ_IF_SOME(u, keyDataJwk.use) {
833 JSG_REQUIRE(u == "enc", DOMDataError,
834 "Symmetric \"jwk\" key must have a \"use\" of \"enc\", not \"", u, "\".");
835 }
836 }
837 
838 KJ_IF_SOME(ops, keyDataJwk.key_ops) {
839 std::sort(ops.begin(), ops.end());
840 // Don't want to use the trick above to use a red-black tree because that constructs the set
841 // once ever for the process, but this path is dependent on user input. Could write things
842 // without the sort but it makes the enforcement from Section 4.2 below a 1-liner.
843 
844 auto duplicate = std::adjacent_find(ops.begin(), ops.end());
845 JSG_REQUIRE(duplicate == ops.end(), DOMDataError,
846 "Symmetric \"jwk\" key contains duplicate value \"", *duplicate, "\", in \"key_op\".");
847 // https://tools.ietf.org/html/rfc7517#section-4.2 - no duplicate values in key_ops.
848 
849 for (const auto& usage: keyUsages) {
850 JSG_REQUIRE(std::binary_search(ops.begin(), ops.end(), usage), DOMDataError,
851 "\"jwk\" key missing usage \"", usage, "\", in \"key_ops\".");
852 }
853 }
854 
855 // TODO(conform/review): How should this from the standard:
856 // > The "use" and "key_ops" JWK members SHOULD NOT be used together;
857 // > however, if both are used, the information they convey MUST be
858 // > consistent
859 // be interpreted? What constitutes "inconsistency"? Is that implicit in enforcing that "enc"
860 // must be the value for `use'? Or is there something else?
861 
862 KJ_IF_SOME(e, keyDataJwk.ext) {
863 JSG_REQUIRE(e || !extractable, DOMDataError, "\"jwk\" key has value \"", e ? "true" : "false",
864 "\", for \"ext\" that is incompatible "
865 "with import extractability value \"",
866 extractable ? "true" : "false", "\".");
867 }
868 } else {
869 JSG_FAIL_REQUIRE(DOMNotSupportedError, "Unrecognized key import format \"", format, "\".");
870 }
871 
872 auto keySize = keyDataArray.size() * 8;
873 KJ_ASSERT(keySize == 128 || keySize == 192 || keySize == 256);
874 
875 auto keyAlgorithm = CryptoKey::AesKeyAlgorithm{normalizedName, static_cast<uint16_t>(keySize)};
876 
877 if (normalizedName == "AES-GCM") {
878 return kj::heap<AesGcmKey>(kj::mv(keyDataArray), kj::mv(keyAlgorithm), extractable, usages);
879 } else if (normalizedName == "AES-CBC") {
880 return kj::heap<AesCbcKey>(kj::mv(keyDataArray), kj::mv(keyAlgorithm), extractable, usages);
881 } else if (normalizedName == "AES-CTR") {
882 return kj::heap<AesCtrKey>(kj::mv(keyDataArray), kj::mv(keyAlgorithm), extractable, usages);
883 } else if (normalizedName == "AES-KW") {
884 return kj::heap<AesKwKey>(kj::mv(keyDataArray), kj::mv(keyAlgorithm), extractable, usages);
885 }
886 
887 JSG_FAIL_REQUIRE(
888 DOMNotSupportedError, "Unsupported algorithm \"", normalizedName, "\" to import.");
889}
890 
891} // namespace workerd::api