| #include "crypto/crypto_aes.h" |
| #include "crypto/crypto_cipher.h" |
| #include "crypto/crypto_keys.h" |
| #include "crypto/crypto_util.h" |
| #include "allocated_buffer-inl.h" |
| #include "async_wrap-inl.h" |
| #include "base_object-inl.h" |
| #include "env-inl.h" |
| #include "memory_tracker-inl.h" |
| #include "threadpoolwork-inl.h" |
| #include "v8.h" |
| |
| #include <openssl/bn.h> |
| #include <openssl/aes.h> |
| |
| #include <vector> |
| |
| namespace node { |
| |
| using v8::FunctionCallbackInfo; |
| using v8::Just; |
| using v8::Local; |
| using v8::Maybe; |
| using v8::Nothing; |
| using v8::Object; |
| using v8::Uint32; |
| using v8::Value; |
| |
| namespace crypto { |
| namespace { |
| // Implements general AES encryption and decryption for CBC |
| // The key_data must be a secret key. |
| // On success, this function sets out to a new AllocatedBuffer |
| // instance containing the results and returns WebCryptoCipherStatus::OK. |
| WebCryptoCipherStatus AES_Cipher( |
| Environment* env, |
| KeyObjectData* key_data, |
| WebCryptoCipherMode cipher_mode, |
| const AESCipherConfig& params, |
| const ByteSource& in, |
| ByteSource* out) { |
| CHECK_NOT_NULL(key_data); |
| CHECK_EQ(key_data->GetKeyType(), kKeyTypeSecret); |
| |
| const int mode = EVP_CIPHER_mode(params.cipher); |
| |
| CipherCtxPointer ctx(EVP_CIPHER_CTX_new()); |
| EVP_CIPHER_CTX_init(ctx.get()); |
| if (mode == EVP_CIPH_WRAP_MODE) |
| EVP_CIPHER_CTX_set_flags(ctx.get(), EVP_CIPHER_CTX_FLAG_WRAP_ALLOW); |
| |
| const bool encrypt = cipher_mode == kWebCryptoCipherEncrypt; |
| |
| if (!EVP_CipherInit_ex( |
| ctx.get(), |
| params.cipher, |
| nullptr, |
| nullptr, |
| nullptr, |
| encrypt)) { |
| // Cipher init failed |
| return WebCryptoCipherStatus::FAILED; |
| } |
| |
| if (mode == EVP_CIPH_GCM_MODE && !EVP_CIPHER_CTX_ctrl( |
| ctx.get(), |
| EVP_CTRL_AEAD_SET_IVLEN, |
| params.iv.size(), |
| nullptr)) { |
| return WebCryptoCipherStatus::FAILED; |
| } |
| |
| if (!EVP_CIPHER_CTX_set_key_length( |
| ctx.get(), |
| key_data->GetSymmetricKeySize()) || |
| !EVP_CipherInit_ex( |
| ctx.get(), |
| nullptr, |
| nullptr, |
| reinterpret_cast<const unsigned char*>(key_data->GetSymmetricKey()), |
| params.iv.data<unsigned char>(), |
| encrypt)) { |
| return WebCryptoCipherStatus::FAILED; |
| } |
| |
| size_t tag_len = 0; |
| |
| if (mode == EVP_CIPH_GCM_MODE) { |
| switch (cipher_mode) { |
| case kWebCryptoCipherDecrypt: |
| // If in decrypt mode, the auth tag must be set in the params.tag. |
| CHECK(params.tag); |
| if (!EVP_CIPHER_CTX_ctrl( |
| ctx.get(), |
| EVP_CTRL_AEAD_SET_TAG, |
| params.tag.size(), |
| const_cast<char*>(params.tag.get()))) { |
| return WebCryptoCipherStatus::FAILED; |
| } |
| break; |
| case kWebCryptoCipherEncrypt: |
| // In decrypt mode, we grab the tag length here. We'll use it to |
| // ensure that that allocated buffer has enough room for both the |
| // final block and the auth tag. Unlike our other AES-GCM implementation |
| // in CipherBase, in WebCrypto, the auth tag is concatentated to the end |
| // of the generated ciphertext and returned in the same ArrayBuffer. |
| tag_len = params.length; |
| break; |
| default: |
| UNREACHABLE(); |
| } |
| } |
| |
| size_t total = 0; |
| int buf_len = in.size() + EVP_CIPHER_CTX_block_size(ctx.get()) + tag_len; |
| int out_len; |
| |
| if (mode == EVP_CIPH_GCM_MODE && |
| params.additional_data.size() && |
| !EVP_CipherUpdate( |
| ctx.get(), |
| nullptr, |
| &out_len, |
| params.additional_data.data<unsigned char>(), |
| params.additional_data.size())) { |
| return WebCryptoCipherStatus::FAILED; |
| } |
| |
| char* data = MallocOpenSSL<char>(buf_len); |
| ByteSource buf = ByteSource::Allocated(data, buf_len); |
| unsigned char* ptr = reinterpret_cast<unsigned char*>(data); |
| |
| if (!EVP_CipherUpdate( |
| ctx.get(), |
| ptr, |
| &out_len, |
| in.data<unsigned char>(), |
| in.size())) { |
| return WebCryptoCipherStatus::FAILED; |
| } |
| |
| total += out_len; |
| CHECK_LE(out_len, buf_len); |
| ptr += out_len; |
| out_len = EVP_CIPHER_CTX_block_size(ctx.get()); |
| if (!EVP_CipherFinal_ex(ctx.get(), ptr, &out_len)) { |
| return WebCryptoCipherStatus::FAILED; |
| } |
| total += out_len; |
| |
| // If using AES_GCM, grab the generated auth tag and append |
| // it to the end of the ciphertext. |
| if (cipher_mode == kWebCryptoCipherEncrypt && mode == EVP_CIPH_GCM_MODE) { |
| data += out_len; |
| if (!EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_AEAD_GET_TAG, tag_len, ptr)) |
| return WebCryptoCipherStatus::FAILED; |
| total += tag_len; |
| } |
| |
| // It's possible that we haven't used the full allocated space. Size down. |
| buf.Resize(total); |
| *out = std::move(buf); |
| |
| return WebCryptoCipherStatus::OK; |
| } |
| |
| // The AES_CTR implementation here takes it's inspiration from the chromium |
| // implementation here: |
| // https://github.com/chromium/chromium/blob/7af6cfd/components/webcrypto/algorithms/aes_ctr.cc |
| |
| template <typename T> |
| T CeilDiv(T a, T b) { |
| return a == 0 ? 0 : 1 + (a - 1) / b; |
| } |
| |
| BignumPointer GetCounter(const AESCipherConfig& params) { |
| unsigned int remainder = (params.length % CHAR_BIT); |
| const unsigned char* data = params.iv.data<unsigned char>(); |
| |
| if (remainder == 0) { |
| unsigned int byte_length = params.length / CHAR_BIT; |
| return BignumPointer(BN_bin2bn( |
| data + params.iv.size() - byte_length, |
| byte_length, |
| nullptr)); |
| } |
| |
| unsigned int byte_length = |
| CeilDiv(params.length, static_cast<size_t>(CHAR_BIT)); |
| |
| std::vector<unsigned char> counter( |
| data + params.iv.size() - byte_length, |
| data + params.iv.size()); |
| counter[0] &= ~(0xFF << remainder); |
| |
| return BignumPointer(BN_bin2bn(counter.data(), counter.size(), nullptr)); |
| } |
| |
| std::vector<unsigned char> BlockWithZeroedCounter( |
| const AESCipherConfig& params) { |
| unsigned int length_bytes = params.length / CHAR_BIT; |
| unsigned int remainder = params.length % CHAR_BIT; |
| |
| const unsigned char* data = params.iv.data<unsigned char>(); |
| |
| std::vector<unsigned char> new_counter_block(data, data + params.iv.size()); |
| |
| size_t index = new_counter_block.size() - length_bytes; |
| memset(&new_counter_block.front() + index, 0, length_bytes); |
| |
| if (remainder) |
| new_counter_block[index - 1] &= 0xFF << remainder; |
| |
| return new_counter_block; |
| } |
| |
| WebCryptoCipherStatus AES_CTR_Cipher2( |
| KeyObjectData* key_data, |
| WebCryptoCipherMode cipher_mode, |
| const AESCipherConfig& params, |
| const ByteSource& in, |
| unsigned const char* counter, |
| unsigned char* out) { |
| CipherCtxPointer ctx(EVP_CIPHER_CTX_new()); |
| const bool encrypt = cipher_mode == kWebCryptoCipherEncrypt; |
| |
| if (!EVP_CipherInit_ex( |
| ctx.get(), |
| params.cipher, |
| nullptr, |
| reinterpret_cast<const unsigned char*>(key_data->GetSymmetricKey()), |
| counter, |
| encrypt)) { |
| // Cipher init failed |
| return WebCryptoCipherStatus::FAILED; |
| } |
| |
| int out_len = 0; |
| int final_len = 0; |
| if (!EVP_CipherUpdate( |
| ctx.get(), |
| out, |
| &out_len, |
| in.data<unsigned char>(), |
| in.size())) { |
| return WebCryptoCipherStatus::FAILED; |
| } |
| |
| if (!EVP_CipherFinal_ex(ctx.get(), out + out_len, &final_len)) |
| return WebCryptoCipherStatus::FAILED; |
| |
| out_len += final_len; |
| if (static_cast<unsigned>(out_len) != in.size()) |
| return WebCryptoCipherStatus::FAILED; |
| |
| return WebCryptoCipherStatus::OK; |
| } |
| |
| WebCryptoCipherStatus AES_CTR_Cipher( |
| Environment* env, |
| KeyObjectData* key_data, |
| WebCryptoCipherMode cipher_mode, |
| const AESCipherConfig& params, |
| const ByteSource& in, |
| ByteSource* out) { |
| BignumPointer num_counters(BN_new()); |
| if (!BN_lshift(num_counters.get(), BN_value_one(), params.length)) |
| return WebCryptoCipherStatus::FAILED; |
| |
| BignumPointer current_counter = GetCounter(params); |
| |
| BignumPointer num_output(BN_new()); |
| |
| if (!BN_set_word(num_output.get(), CeilDiv(in.size(), kAesBlockSize))) |
| return WebCryptoCipherStatus::FAILED; |
| |
| // Just like in chromium's implementation, if the counter will |
| // be incremented more than there are counter values, we fail. |
| if (BN_cmp(num_output.get(), num_counters.get()) > 0) |
| return WebCryptoCipherStatus::FAILED; |
| |
| BignumPointer remaining_until_reset(BN_new()); |
| if (!BN_sub(remaining_until_reset.get(), |
| num_counters.get(), |
| current_counter.get())) { |
| return WebCryptoCipherStatus::FAILED; |
| } |
| |
| // Output size is identical to the input size |
| char* data = MallocOpenSSL<char>(in.size()); |
| ByteSource buf = ByteSource::Allocated(data, in.size()); |
| unsigned char* ptr = reinterpret_cast<unsigned char*>(data); |
| |
| // Also just like in chromium's implementation, if we can process |
| // the input without wrapping the counter, we'll do it as a single |
| // call here. If we can't, we'll fallback to the a two-step approach |
| if (BN_cmp(remaining_until_reset.get(), num_output.get()) >= 0) { |
| auto status = AES_CTR_Cipher2( |
| key_data, |
| cipher_mode, |
| params, |
| in, |
| params.iv.data<unsigned char>(), |
| ptr); |
| if (status == WebCryptoCipherStatus::OK) |
| *out = std::move(buf); |
| return status; |
| } |
| |
| BN_ULONG blocks_part1 = BN_get_word(remaining_until_reset.get()); |
| BN_ULONG input_size_part1 = blocks_part1 * kAesBlockSize; |
| |
| // Encrypt the first part... |
| auto status = AES_CTR_Cipher2( |
| key_data, |
| cipher_mode, |
| params, |
| ByteSource::Foreign(in.get(), input_size_part1), |
| params.iv.data<unsigned char>(), |
| ptr); |
| |
| if (status != WebCryptoCipherStatus::OK) |
| return status; |
| |
| // Wrap the counter around to zero |
| std::vector<unsigned char> new_counter_block = BlockWithZeroedCounter(params); |
| |
| // Encrypt the second part... |
| status = AES_CTR_Cipher2( |
| key_data, |
| cipher_mode, |
| params, |
| ByteSource::Foreign( |
| in.get() + input_size_part1, |
| in.size() - input_size_part1), |
| new_counter_block.data(), |
| ptr + input_size_part1); |
| |
| if (status == WebCryptoCipherStatus::OK) |
| *out = std::move(buf); |
| |
| return status; |
| } |
| |
| bool ValidateIV( |
| Environment* env, |
| CryptoJobMode mode, |
| Local<Value> value, |
| AESCipherConfig* params) { |
| ArrayBufferOrViewContents<char> iv(value); |
| if (UNLIKELY(!iv.CheckSizeInt32())) { |
| THROW_ERR_OUT_OF_RANGE(env, "iv is too big"); |
| return false; |
| } |
| params->iv = (mode == kCryptoJobAsync) |
| ? iv.ToCopy() |
| : iv.ToByteSource(); |
| return true; |
| } |
| |
| bool ValidateCounter( |
| Environment* env, |
| Local<Value> value, |
| AESCipherConfig* params) { |
| CHECK(value->IsUint32()); // Length |
| params->length = value.As<Uint32>()->Value(); |
| if (params->iv.size() != 16 || |
| params->length == 0 || |
| params->length > 128) { |
| THROW_ERR_CRYPTO_INVALID_COUNTER(env); |
| return false; |
| } |
| return true; |
| } |
| |
| bool ValidateAuthTag( |
| Environment* env, |
| CryptoJobMode mode, |
| WebCryptoCipherMode cipher_mode, |
| Local<Value> value, |
| AESCipherConfig* params) { |
| switch (cipher_mode) { |
| case kWebCryptoCipherDecrypt: { |
| if (!IsAnyByteSource(value)) { |
| THROW_ERR_CRYPTO_INVALID_TAG_LENGTH(env); |
| return false; |
| } |
| ArrayBufferOrViewContents<char> tag_contents(value); |
| if (UNLIKELY(!tag_contents.CheckSizeInt32())) { |
| THROW_ERR_OUT_OF_RANGE(env, "tagLength is too big"); |
| return false; |
| } |
| params->tag = mode == kCryptoJobAsync |
| ? tag_contents.ToCopy() |
| : tag_contents.ToByteSource(); |
| break; |
| } |
| case kWebCryptoCipherEncrypt: { |
| if (!value->IsUint32()) { |
| THROW_ERR_CRYPTO_INVALID_TAG_LENGTH(env); |
| return false; |
| } |
| params->length = value.As<Uint32>()->Value(); |
| if (params->length > 128) { |
| THROW_ERR_CRYPTO_INVALID_TAG_LENGTH(env); |
| return false; |
| } |
| break; |
| } |
| default: |
| UNREACHABLE(); |
| } |
| return true; |
| } |
| |
| bool ValidateAdditionalData( |
| Environment* env, |
| CryptoJobMode mode, |
| Local<Value> value, |
| AESCipherConfig* params) { |
| // Additional Data |
| if (IsAnyByteSource(value)) { |
| ArrayBufferOrViewContents<char> additional(value); |
| if (UNLIKELY(!additional.CheckSizeInt32())) { |
| THROW_ERR_OUT_OF_RANGE(env, "additionalData is too big"); |
| return false; |
| } |
| params->additional_data = mode == kCryptoJobAsync |
| ? additional.ToCopy() |
| : additional.ToByteSource(); |
| } |
| return true; |
| } |
| |
| void UseDefaultIV(AESCipherConfig* params) { |
| params->iv = ByteSource::Foreign(kDefaultWrapIV, strlen(kDefaultWrapIV)); |
| } |
| } // namespace |
| |
| AESCipherConfig::AESCipherConfig(AESCipherConfig&& other) noexcept |
| : mode(other.mode), |
| variant(other.variant), |
| cipher(other.cipher), |
| length(other.length), |
| iv(std::move(other.iv)), |
| additional_data(std::move(other.additional_data)), |
| tag(std::move(other.tag)) {} |
| |
| AESCipherConfig& AESCipherConfig::operator=(AESCipherConfig&& other) noexcept { |
| if (&other == this) return *this; |
| this->~AESCipherConfig(); |
| return *new (this) AESCipherConfig(std::move(other)); |
| } |
| |
| void AESCipherConfig::MemoryInfo(MemoryTracker* tracker) const { |
| // If mode is sync, then the data in each of these properties |
| // is not owned by the AESCipherConfig, so we ignore it. |
| if (mode == kCryptoJobAsync) { |
| tracker->TrackFieldWithSize("iv", iv.size()); |
| tracker->TrackFieldWithSize("additional_data", additional_data.size()); |
| tracker->TrackFieldWithSize("tag", tag.size()); |
| } |
| } |
| |
| Maybe<bool> AESCipherTraits::AdditionalConfig( |
| CryptoJobMode mode, |
| const FunctionCallbackInfo<Value>& args, |
| unsigned int offset, |
| WebCryptoCipherMode cipher_mode, |
| AESCipherConfig* params) { |
| Environment* env = Environment::GetCurrent(args); |
| |
| params->mode = mode; |
| |
| CHECK(args[offset]->IsUint32()); // Key Variant |
| params->variant = |
| static_cast<AESKeyVariant>(args[offset].As<Uint32>()->Value()); |
| |
| int cipher_nid; |
| |
| switch (params->variant) { |
| case kKeyVariantAES_CTR_128: |
| if (!ValidateIV(env, mode, args[offset + 1], params) || |
| !ValidateCounter(env, args[offset + 2], params)) { |
| return Nothing<bool>(); |
| } |
| cipher_nid = NID_aes_128_ctr; |
| break; |
| case kKeyVariantAES_CTR_192: |
| if (!ValidateIV(env, mode, args[offset + 1], params) || |
| !ValidateCounter(env, args[offset + 2], params)) { |
| return Nothing<bool>(); |
| } |
| cipher_nid = NID_aes_192_ctr; |
| break; |
| case kKeyVariantAES_CTR_256: |
| if (!ValidateIV(env, mode, args[offset + 1], params) || |
| !ValidateCounter(env, args[offset + 2], params)) { |
| return Nothing<bool>(); |
| } |
| cipher_nid = NID_aes_256_ctr; |
| break; |
| case kKeyVariantAES_CBC_128: |
| if (!ValidateIV(env, mode, args[offset + 1], params)) |
| return Nothing<bool>(); |
| cipher_nid = NID_aes_128_cbc; |
| break; |
| case kKeyVariantAES_CBC_192: |
| if (!ValidateIV(env, mode, args[offset + 1], params)) |
| return Nothing<bool>(); |
| cipher_nid = NID_aes_192_cbc; |
| break; |
| case kKeyVariantAES_CBC_256: |
| if (!ValidateIV(env, mode, args[offset + 1], params)) |
| return Nothing<bool>(); |
| cipher_nid = NID_aes_256_cbc; |
| break; |
| case kKeyVariantAES_KW_128: |
| UseDefaultIV(params); |
| cipher_nid = NID_id_aes128_wrap; |
| break; |
| case kKeyVariantAES_KW_192: |
| UseDefaultIV(params); |
| cipher_nid = NID_id_aes192_wrap; |
| break; |
| case kKeyVariantAES_KW_256: |
| UseDefaultIV(params); |
| cipher_nid = NID_id_aes256_wrap; |
| break; |
| case kKeyVariantAES_GCM_128: |
| if (!ValidateIV(env, mode, args[offset + 1], params) || |
| !ValidateAuthTag(env, mode, cipher_mode, args[offset + 2], params) || |
| !ValidateAdditionalData(env, mode, args[offset + 3], params)) { |
| return Nothing<bool>(); |
| } |
| cipher_nid = NID_aes_128_gcm; |
| break; |
| case kKeyVariantAES_GCM_192: |
| if (!ValidateIV(env, mode, args[offset + 1], params) || |
| !ValidateAuthTag(env, mode, cipher_mode, args[offset + 2], params) || |
| !ValidateAdditionalData(env, mode, args[offset + 3], params)) { |
| return Nothing<bool>(); |
| } |
| cipher_nid = NID_aes_192_gcm; |
| break; |
| case kKeyVariantAES_GCM_256: |
| if (!ValidateIV(env, mode, args[offset + 1], params) || |
| !ValidateAuthTag(env, mode, cipher_mode, args[offset + 2], params) || |
| !ValidateAdditionalData(env, mode, args[offset + 3], params)) { |
| return Nothing<bool>(); |
| } |
| cipher_nid = NID_aes_256_gcm; |
| break; |
| default: |
| UNREACHABLE(); |
| } |
| |
| params->cipher = EVP_get_cipherbynid(cipher_nid); |
| CHECK_NOT_NULL(params->cipher); |
| |
| if (params->iv.size() < |
| static_cast<size_t>(EVP_CIPHER_iv_length(params->cipher))) { |
| THROW_ERR_CRYPTO_INVALID_IV(env); |
| return Nothing<bool>(); |
| } |
| |
| return Just(true); |
| } |
| |
| WebCryptoCipherStatus AESCipherTraits::DoCipher( |
| Environment* env, |
| std::shared_ptr<KeyObjectData> key_data, |
| WebCryptoCipherMode cipher_mode, |
| const AESCipherConfig& params, |
| const ByteSource& in, |
| ByteSource* out) { |
| #define V(name, fn) \ |
| case kKeyVariantAES_ ## name: \ |
| return fn(env, key_data.get(), cipher_mode, params, in, out); |
| switch (params.variant) { |
| VARIANTS(V) |
| default: |
| UNREACHABLE(); |
| } |
| #undef V |
| } |
| |
| void AES::Initialize(Environment* env, Local<Object> target) { |
| AESCryptoJob::Initialize(env, target); |
| |
| #define V(name, _) NODE_DEFINE_CONSTANT(target, kKeyVariantAES_ ## name); |
| VARIANTS(V) |
| #undef V |
| } |
| |
| } // namespace crypto |
| } // namespace node |