File
Blob: src/workerd/api/crypto/aes.c++
| 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 | |
| 20 | namespace workerd::api { |
| 21 | namespace { |
| 22 | auto 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 | |
| 36 | auto 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. |
| 51 | void 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 | |
| 66 | int 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. |
| 111 | class 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 | |
| 197 | class 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 | |
| 353 | class 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 | |
| 442 | class 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 | |
| 645 | class 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 | |
| 720 | CryptoKeyUsageSet 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 | |
| 734 | kj::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 | |
| 777 | kj::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 |