#include "crypto/crypto_cipher.h"
#include "base_object-inl.h"
#include "crypto/crypto_util.h"
#include "env-inl.h"
#include "memory_tracker-inl.h"
#include "node_buffer.h"
#include "node_internals.h"
#include "node_process-inl.h"
#include "v8.h"
#include <algorithm>
#include <string>
#include <vector>
namespace node {
using ncrypto::Cipher;
using ncrypto::CipherCtxPointer;
using ncrypto::ClearErrorOnReturn;
using ncrypto::Digest;
using ncrypto::EVPKeyCtxPointer;
using ncrypto::EVPKeyPointer;
using ncrypto::MarkPopErrorOnReturn;
using ncrypto::SSLCtxPointer;
using ncrypto::SSLPointer;
using v8::Array;
using v8::ArrayBuffer;
using v8::BackingStore;
using v8::BackingStoreInitializationMode;
using v8::Context;
using v8::DictionaryTemplate;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::HandleScope;
using v8::Int32;
using v8::Isolate;
using v8::Local;
using v8::LocalVector;
using v8::MaybeLocal;
using v8::Object;
using v8::Uint32;
using v8::Undefined;
using v8::Value;
namespace crypto {
namespace {
// Collects and returns information on the given cipher
void GetCipherInfo(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
auto tmpl = env->cipherinfo_detail_template();
if (tmpl.IsEmpty()) {
static constexpr std::string_view names[] = {
"mode",
"name",
"nid",
"keyLength",
"blockSize",
"ivLength",
};
tmpl = DictionaryTemplate::New(env->isolate(), names);
env->set_cipherinfo_detail_template(tmpl);
}
MaybeLocal<Value> values[] = {
Undefined(env->isolate()), // mode
Undefined(env->isolate()), // name
Undefined(env->isolate()), // nid
Undefined(env->isolate()), // keyLength
Undefined(env->isolate()), // blockSize
Undefined(env->isolate()), // ivLength
};
CHECK(args[0]->IsString() || args[0]->IsInt32());
const auto cipher = ([&] {
if (args[0]->IsString()) {
Utf8Value name(env->isolate(), args[0]);
return Cipher::FromName(*name, env->provider_cipher_cache.get());
} else {
int nid = args[0].As<Int32>()->Value();
return Cipher::FromNid(nid, env->provider_cipher_cache.get());
}
})();
if (!cipher) return;
size_t iv_length = cipher.getIvLength();
size_t key_length = cipher.getKeyLength();
// If the testKeyLen and testIvLen arguments are specified,
// then we will make an attempt to see if they are usable for
// the cipher in question, returning undefined if they are not.
// If they are, the info object will be returned with the values
// given.
if (args[1]->IsUint32() || args[2]->IsUint32()) {
// Test and input IV or key length to determine if it's acceptable.
// If it is, then the getCipherInfo will succeed with the given
// values.
auto ctx = CipherCtxPointer::New();
if (!ctx) {
return THROW_ERR_CRYPTO_OPERATION_FAILED(
env, "Failed to allocate cipher context");
}
if (!ctx.init(cipher, true)) {
return;
}
if (args[1]->IsUint32()) {
size_t check_len = args[1].As<Uint32>()->Value();
if (!ctx.setKeyLength(check_len)) {
return;
}
key_length = check_len;
}
if (args[2]->IsUint32()) {
size_t check_len = args[2].As<Uint32>()->Value();
// For CCM modes, the IV may be between 7 and 13 bytes.
// For GCM modes, any IV length is accepted. For OCB modes, we'll check
// by attempting to set the value. For everything else, just check that
// check_len == iv_length.
if (cipher.isCcmMode()) {
if (check_len < 7 || check_len > 13) return;
} else if (cipher.isGcmMode()) {
// Nothing to do.
} else if (cipher.isOcbMode()) {
if (!ctx.setIvLength(check_len)) return;
} else if (cipher.isSivMode() || cipher.isGcmSivMode()) {
if (check_len != iv_length) return;
} else {
if (check_len != iv_length) return;
}
iv_length = check_len;
}
}
// Lowercase the name in place before we create the JS string from it.
std::string name_str(cipher.getName());
name_str = ToLower(name_str);
values[0] = ToV8Value(env->context(), cipher.getModeLabel(), env->isolate());
values[1] = ToV8Value(env->context(), name_str, env->isolate());
const int nid = cipher.getNid();
if (nid != NID_undef) {
values[2] = Uint32::NewFromUnsigned(env->isolate(), nid);
}
values[3] = Uint32::NewFromUnsigned(env->isolate(), key_length);
// Stream ciphers do not have a meaningful block size
if (!cipher.isStreamMode()) {
values[4] = Uint32::NewFromUnsigned(env->isolate(), cipher.getBlockSize());
}
// Ciphers that do not use an IV shouldn't report a length
if (iv_length != 0) {
values[5] = Uint32::NewFromUnsigned(env->isolate(), iv_length);
}
Local<Object> info;
if (NewDictionaryInstanceNullProto(env->context(), tmpl, values)
.ToLocal(&info)) {
args.GetReturnValue().Set(info);
}
}
} // namespace
void CipherBase::GetSSLCiphers(const FunctionCallbackInfo<Value>& args) {
ClearErrorOnReturn clear_error_on_return;
Environment* env = Environment::GetCurrent(args);
auto ctx = SSLCtxPointer::New();
if (!ctx) {
return ThrowCryptoError(
env, clear_error_on_return.peekError(), "SSL_CTX_new");
}
auto ssl = SSLPointer::New(ctx);
if (!ssl) {
return ThrowCryptoError(env, clear_error_on_return.peekError(), "SSL_new");
}
LocalVector<Value> arr(env->isolate());
ssl.getCiphers([&](const std::string_view name) {
arr.push_back(OneByteString(env->isolate(), name.data(), name.length()));
});
args.GetReturnValue().Set(Array::New(env->isolate(), arr.data(), arr.size()));
}
void CipherBase::GetCiphers(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
std::vector<std::string> cipher_names;
Cipher::ForEach(
[&](std::string_view name) { cipher_names.emplace_back(name); });
std::sort(cipher_names.begin(), cipher_names.end());
cipher_names.erase(std::unique(cipher_names.begin(), cipher_names.end()),
cipher_names.end());
LocalVector<Value> ciphers(env->isolate());
bool errored = false;
for (const std::string& name : cipher_names) {
if (errored) break;
Local<Value> val;
if (!ToV8Value(env->context(), name, env->isolate()).ToLocal(&val)) {
errored = true;
break;
}
ciphers.push_back(val);
}
// If errored is true here, then we encountered a JavaScript error
// while trying to create the V8 String from the std::string_view
// in the iteration callback. That means we need to throw.
if (!errored) {
args.GetReturnValue().Set(
Array::New(env->isolate(), ciphers.data(), ciphers.size()));
}
}
CipherBase::CipherBase(Environment* env, Local<Object> wrap, CipherKind kind)
: BaseObject(env, wrap),
ctx_(nullptr),
kind_(kind),
auth_tag_state_(kAuthTagUnknown),
auth_tag_len_(kNoAuthTagLength),
pending_auth_failed_(false),
has_one_shot_update_(false),
siv_aad_components_(0) {
MakeWeak();
}
void CipherBase::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackFieldWithSize("context", ctx_ ? kSizeOf_EVP_CIPHER_CTX : 0);
}
void CipherBase::Initialize(Environment* env, Local<Object> target) {
Isolate* isolate = env->isolate();
Local<Context> context = env->context();
Local<FunctionTemplate> t = NewFunctionTemplate(isolate, New);
t->InstanceTemplate()->SetInternalFieldCount(CipherBase::kInternalFieldCount);
SetProtoMethod(isolate, t, "update", Update);
SetProtoMethod(isolate, t, "final", Final);
SetProtoMethod(isolate, t, "setAutoPadding", SetAutoPadding);
SetProtoMethodNoSideEffect(isolate, t, "getAuthTag", GetAuthTag);
SetProtoMethod(isolate, t, "setAuthTag", SetAuthTag);
SetProtoMethod(isolate, t, "setAAD", SetAAD);
SetConstructorFunction(context, target, "CipherBase", t);
SetMethodNoSideEffect(context, target, "getSSLCiphers", GetSSLCiphers);
SetMethodNoSideEffect(context, target, "getCiphers", GetCiphers);
SetMethod(context,
target,
"publicEncrypt",
PublicKeyCipher::Cipher<PublicKeyCipher::kPublic,
ncrypto::Cipher::encrypt>);
SetMethod(context,
target,
"privateDecrypt",
PublicKeyCipher::Cipher<PublicKeyCipher::kPrivate,
ncrypto::Cipher::decrypt>);
SetMethod(context,
target,
"privateEncrypt",
PublicKeyCipher::Cipher<PublicKeyCipher::kPrivate,
ncrypto::Cipher::sign>);
SetMethod(context,
target,
"publicDecrypt",
PublicKeyCipher::Cipher<PublicKeyCipher::kPublic,
ncrypto::Cipher::recover>);
SetMethodNoSideEffect(context, target, "getCipherInfo", GetCipherInfo);
NODE_DEFINE_CONSTANT(target, kWebCryptoCipherEncrypt);
NODE_DEFINE_CONSTANT(target, kWebCryptoCipherDecrypt);
}
void CipherBase::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(New);
registry->Register(Update);
registry->Register(Final);
registry->Register(SetAutoPadding);
registry->Register(GetAuthTag);
registry->Register(SetAuthTag);
registry->Register(SetAAD);
registry->Register(GetSSLCiphers);
registry->Register(GetCiphers);
registry->Register(PublicKeyCipher::Cipher<PublicKeyCipher::kPublic,
ncrypto::Cipher::encrypt>);
registry->Register(PublicKeyCipher::Cipher<PublicKeyCipher::kPrivate,
ncrypto::Cipher::decrypt>);
registry->Register(PublicKeyCipher::Cipher<PublicKeyCipher::kPrivate,
ncrypto::Cipher::sign>);
registry->Register(PublicKeyCipher::Cipher<PublicKeyCipher::kPublic,
ncrypto::Cipher::recover>);
registry->Register(GetCipherInfo);
}
void CipherBase::New(const FunctionCallbackInfo<Value>& args) {
CHECK(args.IsConstructCall());
Environment* env = Environment::GetCurrent(args);
CHECK(args.Length() == 5 || args.Length() == 7);
CipherBase* cipher =
new CipherBase(env, args.This(), args[0]->IsTrue() ? kCipher : kDecipher);
const Utf8Value cipher_type(env->isolate(), args[1]);
// The argument can either be a KeyObjectHandle or a byte source
// (e.g. ArrayBuffer, TypedArray, etc). Whichever it is, grab the
// raw bytes and proceed...
const ByteSource key_buf = ByteSource::FromSecretKeyBytes(env, args[2]);
if (key_buf.size() > INT_MAX) [[unlikely]] {
return THROW_ERR_OUT_OF_RANGE(env, "key is too big");
}
ArrayBufferOrViewContents<unsigned char> iv_buf(
!args[3]->IsNull() ? args[3] : Local<Value>());
if (!iv_buf.CheckSizeInt32()) [[unlikely]] {
return THROW_ERR_OUT_OF_RANGE(env, "iv is too big");
}
// Don't assign to cipher->auth_tag_len_ directly; the value might not
// represent a valid length at this point.
unsigned int auth_tag_len;
if (args[4]->IsUint32()) {
auth_tag_len = args[4].As<Uint32>()->Value();
} else {
CHECK(args[4]->IsInt32() && args[4].As<Int32>()->Value() == -1);
auth_tag_len = kNoAuthTagLength;
}
if (args.Length() == 5) {
cipher->InitIv(
*cipher_type, key_buf, iv_buf, auth_tag_len, nullptr, nullptr);
return;
}
CHECK(args[5]->IsString() || args[5]->IsUndefined());
CHECK(args[6]->IsString() || args[6]->IsUndefined());
const Utf8Value cts_mode(env->isolate(),
args[5]->IsString() ? args[5] : Local<Value>());
const Utf8Value xts_standard(env->isolate(),
args[6]->IsString() ? args[6] : Local<Value>());
cipher->InitIv(*cipher_type,
key_buf,
iv_buf,
auth_tag_len,
args[5]->IsString() ? *cts_mode : nullptr,
args[6]->IsString() ? *xts_standard : nullptr);
}
void CipherBase::CommonInit(const char* cipher_type,
const ncrypto::Cipher& cipher,
const unsigned char* key,
int key_len,
const unsigned char* iv,
int iv_len,
unsigned int auth_tag_len,
const char* cts_mode,
const char* xts_standard) {
MarkPopErrorOnReturn mark_pop_error_on_return;
CHECK(!ctx_);
ctx_ = CipherCtxPointer::New();
if (!ctx_) {
return THROW_ERR_CRYPTO_OPERATION_FAILED(
env(), "Failed to allocate cipher context");
}
if (cipher.isWrapMode()) {
ctx_.setAllowWrap();
}
const bool encrypt = (kind_ == kCipher);
if (!ctx_.init(cipher, encrypt)) {
return ThrowCryptoError(env(),
mark_pop_error_on_return.peekError(),
"Failed to initialize cipher");
}
if (cts_mode != nullptr && !ctx_.setCtsMode(cts_mode)) {
ctx_.reset();
return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(
env(), "%s does not support the ctsMode option", cipher_type);
}
if (xts_standard != nullptr && !ctx_.setXtsStandard(xts_standard)) {
ctx_.reset();
return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(
env(), "%s does not support the xtsStandard option", cipher_type);
}
if (cipher.isSupportedAuthenticatedMode()) {
CHECK_GE(iv_len, 0);
if (!InitAuthenticated(cipher_type, iv_len, auth_tag_len)) {
return;
}
}
if (!ctx_.setKeyLength(key_len)) {
ctx_.reset();
return THROW_ERR_CRYPTO_INVALID_KEYLEN(env());
}
if (!ctx_.init(Cipher(), encrypt, key, iv)) {
return ThrowCryptoError(env(),
mark_pop_error_on_return.peekError(),
"Failed to initialize cipher");
}
}
void CipherBase::InitIv(const char* cipher_type,
const ByteSource& key_buf,
const ArrayBufferOrViewContents<unsigned char>& iv_buf,
unsigned int auth_tag_len,
const char* cts_mode,
const char* xts_standard) {
HandleScope scope(env()->isolate());
MarkPopErrorOnReturn mark_pop_error_on_return;
auto cipher =
Cipher::FromName(cipher_type, env()->provider_cipher_cache.get());
if (!cipher) return THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env());
const int expected_iv_len = cipher.getIvLength();
const bool has_iv = iv_buf.size() > 0;
// Throw if no IV was passed and the cipher requires an IV
if (!has_iv && expected_iv_len != 0) {
return THROW_ERR_CRYPTO_INVALID_IV(env());
}
// Throw if an IV was passed which does not match the cipher's fixed IV length
// static_cast<int> for the iv_buf.size() is safe because we've verified
// prior that the value is not larger than INT_MAX.
if (!cipher.isSupportedAuthenticatedMode() && has_iv &&
static_cast<int>(iv_buf.size()) != expected_iv_len) {
return THROW_ERR_CRYPTO_INVALID_IV(env());
}
if (cipher.isChaCha20Poly1305()) {
CHECK(has_iv);
// Check for invalid IV lengths, since OpenSSL does not under some
// conditions:
// https://www.openssl.org/news/secadv/20190306.txt.
if (iv_buf.size() > 12) {
return THROW_ERR_CRYPTO_INVALID_IV(env());
}
}
if (cipher.isSivMode() && has_iv) {
return THROW_ERR_CRYPTO_INVALID_IV(env());
}
if (cipher.isGcmSivMode() &&
(!has_iv || static_cast<int>(iv_buf.size()) != expected_iv_len)) {
return THROW_ERR_CRYPTO_INVALID_IV(env());
}
CommonInit(cipher_type,
cipher,
key_buf.data<unsigned char>(),
key_buf.size(),
iv_buf.data(),
iv_buf.size(),
auth_tag_len,
cts_mode,
xts_standard);
}
bool CipherBase::InitAuthenticated(const char* cipher_type,
int iv_len,
unsigned int auth_tag_len) {
CHECK(IsAuthenticatedMode());
MarkPopErrorOnReturn mark_pop_error_on_return;
const bool is_siv = ctx_.isSivMode() || ctx_.isGcmSivMode();
if (!is_siv && !ctx_.setIvLength(iv_len)) {
THROW_ERR_CRYPTO_INVALID_IV(env());
return false;
}
if (ctx_.isCcmMode()) {
// TODO(tniessen) Support CCM decryption in FIPS mode
if (kind_ == kDecipher && ncrypto::isFipsEnabled()) {
THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(
env(), "CCM decryption not supported in FIPS mode");
return false;
}
// Restrict the message length to min(INT_MAX, 2^(8*(15-iv_len))-1) bytes.
CHECK(iv_len >= 7 && iv_len <= 13);
max_message_size_ = INT_MAX;
if (iv_len == 12) max_message_size_ = 16777215;
if (iv_len == 13) max_message_size_ = 65535;
}
if (auth_tag_len == kNoAuthTagLength) {
// GCM, SIV, and ChaCha20-Poly1305 have a default tag length of 16 bytes.
// Other modes (CCM, OCB) require an explicit tag length.
if (ctx_.isGcmMode() || is_siv) {
auth_tag_len = EVP_GCM_TLS_TAG_LEN;
#ifdef EVP_CHACHAPOLY_TLS_TAG_LEN
} else if (ctx_.isChaCha20Poly1305()) {
auth_tag_len = EVP_CHACHAPOLY_TLS_TAG_LEN;
#endif
} else {
THROW_ERR_CRYPTO_INVALID_AUTH_TAG(
env(), "authTagLength required for %s", cipher_type);
return false;
}
} else {
// GCM authentication tag lengths are restricted according to NIST 800-38d,
// page 9. SIV modes only support 16-byte tags. For other modes, we rely on
// OpenSSL to validate the length.
bool is_invalid_auth_tag_length;
if (is_siv) {
is_invalid_auth_tag_length = auth_tag_len != EVP_GCM_TLS_TAG_LEN;
} else if (ctx_.isGcmMode()) {
is_invalid_auth_tag_length = !Cipher::IsValidGCMTagLength(auth_tag_len);
} else {
is_invalid_auth_tag_length = !ctx_.setAeadTagLength(auth_tag_len);
}
if (is_invalid_auth_tag_length) {
THROW_ERR_CRYPTO_INVALID_AUTH_TAG(
env(), "Invalid authentication tag length: %u", auth_tag_len);
return false;
}
}
// Remember the given authentication tag length for later.
auth_tag_len_ = auth_tag_len;
return true;
}
bool CipherBase::CheckCCMMessageLength(int message_len) {
CHECK(ctx_);
CHECK(ctx_.isCcmMode());
if (message_len > max_message_size_) {
THROW_ERR_CRYPTO_INVALID_MESSAGELEN(env());
return false;
}
return true;
}
bool CipherBase::IsAuthenticatedMode() const {
// Check if this cipher operates in an AEAD mode that we support.
CHECK(ctx_);
return ncrypto::Cipher::FromCtx(ctx_).isSupportedAuthenticatedMode();
}
void CipherBase::GetAuthTag(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CipherBase* cipher;
ASSIGN_OR_RETURN_UNWRAP(&cipher, args.This());
// Only callable after Final and if encrypting.
if (cipher->ctx_ || cipher->kind_ != kCipher ||
cipher->auth_tag_state_ != kAuthTagComputed) {
return;
}
CHECK_NE(cipher->auth_tag_len_, kNoAuthTagLength);
Local<Value> ret;
if (Buffer::Copy(env, cipher->auth_tag_, cipher->auth_tag_len_)
.ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}
void CipherBase::SetAuthTag(const FunctionCallbackInfo<Value>& args) {
CipherBase* cipher;
ASSIGN_OR_RETURN_UNWRAP(&cipher, args.This());
Environment* env = Environment::GetCurrent(args);
if (!cipher->ctx_ ||
!cipher->IsAuthenticatedMode() ||
cipher->kind_ != kDecipher ||
cipher->auth_tag_state_ != kAuthTagUnknown) {
return args.GetReturnValue().Set(false);
}
if ((cipher->ctx_.isSivMode() || cipher->ctx_.isGcmSivMode()) &&
cipher->has_one_shot_update_) {
return args.GetReturnValue().Set(false);
}
ArrayBufferOrViewContents<char> auth_tag(args[0]);
if (!auth_tag.CheckSizeInt32()) [[unlikely]] {
return THROW_ERR_OUT_OF_RANGE(env, "buffer is too big");
}
unsigned int tag_len = auth_tag.size();
// Older versions of Node.js did allow setting the auth tag length implicitly
// for GCM mode (see DEP0182). We now require knowledge of the expected tag
// length up front, so it must have been set during initialization.
// The configured tag length must match the length of the given auth tag.
CHECK_NE(cipher->auth_tag_len_, kNoAuthTagLength);
if (cipher->auth_tag_len_ != tag_len) {
return THROW_ERR_CRYPTO_INVALID_AUTH_TAG(
env, "Invalid authentication tag length: %u", tag_len);
}
CHECK_LE(cipher->auth_tag_len_, ncrypto::Cipher::MAX_AUTH_TAG_LENGTH);
if (!cipher->ctx_.setAeadTag({auth_tag.data(), cipher->auth_tag_len_})) {
return args.GetReturnValue().Set(false);
}
cipher->auth_tag_state_ = kAuthTagSetByUser;
args.GetReturnValue().Set(true);
}
bool CipherBase::SetAAD(
const ArrayBufferOrViewContents<unsigned char>& data,
int plaintext_len) {
if (!ctx_ || !IsAuthenticatedMode())
return false;
const bool is_siv = ctx_.isSivMode();
if ((is_siv || ctx_.isGcmSivMode()) && has_one_shot_update_) return false;
if (is_siv && siv_aad_components_ >= kMaxSivAADComponents) return false;
MarkPopErrorOnReturn mark_pop_error_on_return;
int outlen;
// When in CCM mode, we need to set the authentication tag and the plaintext
// length in advance.
if (ctx_.isCcmMode()) {
if (plaintext_len < 0) {
THROW_ERR_MISSING_ARGS(env(),
"options.plaintextLength required for CCM mode with AAD");
return false;
}
if (!CheckCCMMessageLength(plaintext_len)) {
return false;
}
ncrypto::Buffer<const unsigned char> buffer{
.data = nullptr,
.len = static_cast<size_t>(plaintext_len),
};
// Specify the plaintext length.
if (!ctx_.update(buffer, nullptr, &outlen)) {
return false;
}
}
ncrypto::Buffer<const unsigned char> buffer{
.data = data.data(),
.len = data.size(),
};
const bool success = ctx_.update(buffer, nullptr, &outlen);
if (success && is_siv) siv_aad_components_++;
return success;
}
void CipherBase::SetAAD(const FunctionCallbackInfo<Value>& args) {
CipherBase* cipher;
ASSIGN_OR_RETURN_UNWRAP(&cipher, args.This());
Environment* env = Environment::GetCurrent(args);
CHECK_EQ(args.Length(), 2);
CHECK(args[1]->IsInt32());
int plaintext_len = args[1].As<Int32>()->Value();
ArrayBufferOrViewContents<unsigned char> buf(args[0]);
if (!buf.CheckSizeInt32()) [[unlikely]] {
return THROW_ERR_OUT_OF_RANGE(env, "buffer is too big");
}
args.GetReturnValue().Set(cipher->SetAAD(buf, plaintext_len));
}
CipherBase::UpdateResult CipherBase::Update(
const char* data,
size_t len,
std::unique_ptr<BackingStore>* out) {
if (!ctx_ || len > INT_MAX) return kErrorState;
MarkPopErrorOnReturn mark_pop_error_on_return;
const bool is_ccm_mode = ctx_.isCcmMode();
const bool is_ccm_decipher = kind_ == kDecipher && is_ccm_mode;
if (is_ccm_mode && !CheckCCMMessageLength(len)) {
return kErrorMessageSize;
}
const bool is_siv = ctx_.isSivMode() || ctx_.isGcmSivMode();
const bool is_cts = ctx_.isCtsMode();
const bool is_xts = ctx_.isXtsMode();
const bool is_wrap = ctx_.isWrapMode();
const bool is_one_shot =
is_ccm_decipher || is_siv || is_cts || is_xts || is_wrap;
if (is_one_shot && has_one_shot_update_) {
return kErrorState;
}
const int block_size = ctx_.getBlockSize();
CHECK_GT(block_size, 0);
if (len + block_size > INT_MAX) return kErrorState;
int buf_len = len + block_size;
ncrypto::Buffer<const unsigned char> buffer = {
.data = reinterpret_cast<const unsigned char*>(data),
.len = len,
};
if (kind_ == kCipher && ctx_.isWrapMode() &&
!ctx_.update(buffer, nullptr, &buf_len)) {
return kErrorState;
}
*out = ArrayBuffer::NewBackingStore(
env()->isolate(),
buf_len,
BackingStoreInitializationMode::kUninitialized);
buffer = {
.data = reinterpret_cast<const unsigned char*>(data),
.len = len,
};
bool r = ctx_.update(
buffer, static_cast<unsigned char*>((*out)->Data()), &buf_len);
if (is_ccm_decipher || is_siv || ((is_cts || is_xts || is_wrap) && r)) {
has_one_shot_update_ = true;
}
// When in CCM mode, EVP_CipherUpdate will fail if the authentication tag is
// invalid. AES-SIV reports the same authentication failure at update time,
// but the AEAD contract expects the failure to surface from final().
const bool pending_auth_failed =
!r && kind_ == kDecipher && (ctx_.isCcmMode() || is_siv);
if (pending_auth_failed) {
buf_len = 0;
}
CHECK_LE(static_cast<size_t>(buf_len), (*out)->ByteLength());
if (buf_len == 0) {
*out = ArrayBuffer::NewBackingStore(env()->isolate(), 0);
} else if (static_cast<size_t>(buf_len) != (*out)->ByteLength()) {
std::unique_ptr<BackingStore> old_out = std::move(*out);
*out = ArrayBuffer::NewBackingStore(
env()->isolate(),
buf_len,
BackingStoreInitializationMode::kUninitialized);
memcpy((*out)->Data(), old_out->Data(), buf_len);
}
if (pending_auth_failed) {
pending_auth_failed_ = true;
return kSuccess;
}
return r == 1 ? kSuccess : kErrorState;
}
void CipherBase::Update(const FunctionCallbackInfo<Value>& args) {
Decode<CipherBase>(
args,
[](CipherBase* cipher,
const FunctionCallbackInfo<Value>& args,
const char* data,
size_t size) {
MarkPopErrorOnReturn mark_pop_error_on_return;
std::unique_ptr<BackingStore> out;
Environment* env = Environment::GetCurrent(args);
if (size > INT_MAX) [[unlikely]] {
return THROW_ERR_OUT_OF_RANGE(env, "data is too long");
}
UpdateResult r = cipher->Update(data, size, &out);
if (r != kSuccess) {
if (r == kErrorState) {
ThrowCryptoError(env,
mark_pop_error_on_return.peekError(),
"Trying to add data in unsupported state");
}
return;
}
auto ab = ArrayBuffer::New(env->isolate(), std::move(out));
args.GetReturnValue().Set(Buffer::New(env, ab, 0, ab->ByteLength())
.FromMaybe(Local<Value>()));
});
}
bool CipherBase::SetAutoPadding(bool auto_padding) {
if (!ctx_) return false;
MarkPopErrorOnReturn mark_pop_error_on_return;
return ctx_.setPadding(auto_padding);
}
void CipherBase::SetAutoPadding(const FunctionCallbackInfo<Value>& args) {
CipherBase* cipher;
ASSIGN_OR_RETURN_UNWRAP(&cipher, args.This());
bool b = cipher->SetAutoPadding(args.Length() < 1 || args[0]->IsTrue());
args.GetReturnValue().Set(b); // Possibly report invalid state failure
}
bool CipherBase::Final(std::unique_ptr<BackingStore>* out) {
if (!ctx_) return false;
const bool is_one_shot = ctx_.isSivMode() || ctx_.isGcmSivMode() ||
ctx_.isCtsMode() || ctx_.isXtsMode() ||
ctx_.isWrapMode();
if (is_one_shot && !has_one_shot_update_) {
ctx_.reset();
return false;
}
*out = ArrayBuffer::NewBackingStore(
env()->isolate(),
static_cast<size_t>(ctx_.getBlockSize()),
BackingStoreInitializationMode::kUninitialized);
#ifdef OPENSSL_IS_BORINGSSL
// BoringSSL doesn't verify the presence of the auth tag so do
// it ourselves, see https://github.com/nodejs/node/issues/45874.
if (kind_ == kDecipher && ctx_.isChaCha20Poly1305() &&
auth_tag_state_ != kAuthTagSetByUser) {
return false;
}
#endif
// In CCM mode, final() only checks whether authentication failed in update().
// EVP_CipherFinal_ex must not be called and will fail.
bool ok;
if (kind_ == kDecipher && ctx_.isCcmMode()) {
ok = auth_tag_state_ == kAuthTagSetByUser && has_one_shot_update_ &&
!pending_auth_failed_;
*out = ArrayBuffer::NewBackingStore(env()->isolate(), 0);
} else {
int out_len = (*out)->ByteLength();
ok = ctx_.update(
{}, static_cast<unsigned char*>((*out)->Data()), &out_len, true);
CHECK_LE(static_cast<size_t>(out_len), (*out)->ByteLength());
if (out_len == 0) {
*out = ArrayBuffer::NewBackingStore(env()->isolate(), 0);
} else if (static_cast<size_t>(out_len) != (*out)->ByteLength()) {
std::unique_ptr<BackingStore> old_out = std::move(*out);
*out = ArrayBuffer::NewBackingStore(
env()->isolate(),
out_len,
BackingStoreInitializationMode::kUninitialized);
memcpy((*out)->Data(), old_out->Data(), out_len);
}
if (ok && kind_ == kCipher && IsAuthenticatedMode()) {
CHECK_NE(auth_tag_len_, kNoAuthTagLength);
ok = ctx_.getAeadTag(auth_tag_len_,
reinterpret_cast<unsigned char*>(auth_tag_));
if (ok) {
auth_tag_state_ = kAuthTagComputed;
}
}
}
ctx_.reset();
return ok;
}
void CipherBase::Final(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
MarkPopErrorOnReturn mark_pop_error_on_return;
CipherBase* cipher;
ASSIGN_OR_RETURN_UNWRAP(&cipher, args.This());
if (cipher->ctx_ == nullptr) {
return THROW_ERR_CRYPTO_INVALID_STATE(env);
}
std::unique_ptr<BackingStore> out;
// Check IsAuthenticatedMode() first, Final() destroys the EVP_CIPHER_CTX.
const bool is_auth_mode = cipher->IsAuthenticatedMode();
bool r = cipher->Final(&out);
if (!r) {
const char* msg = is_auth_mode
? "Unsupported state or unable to authenticate data"
: "Unsupported state";
return ThrowCryptoError(env, mark_pop_error_on_return.peekError(), msg);
}
auto ab = ArrayBuffer::New(env->isolate(), std::move(out));
args.GetReturnValue().Set(
Buffer::New(env, ab, 0, ab->ByteLength()).FromMaybe(Local<Value>()));
}
template <PublicKeyCipher::Cipher_t cipher>
bool PublicKeyCipher::Cipher(
Environment* env,
const EVPKeyPointer& pkey,
int padding,
const Digest& digest,
const Digest& mgf1_digest,
const ArrayBufferOrViewContents<unsigned char>& oaep_label,
const ArrayBufferOrViewContents<unsigned char>& data,
std::unique_ptr<BackingStore>* out) {
auto label = oaep_label.ToByteSource();
auto in = data.ToByteSource();
const ncrypto::Cipher::CipherParams params{
.padding = padding,
.digest = digest,
.mgf1_digest = mgf1_digest,
.label = label,
};
auto buf = cipher(pkey, params, in);
if (!buf) return false;
if (buf.size() == 0) {
*out = ArrayBuffer::NewBackingStore(env->isolate(), 0);
} else {
*out = ArrayBuffer::NewBackingStore(
env->isolate(),
buf.size(),
BackingStoreInitializationMode::kUninitialized);
memcpy((*out)->Data(), buf.get(), buf.size());
}
return true;
}
template <PublicKeyCipher::Operation operation,
PublicKeyCipher::Cipher_t cipher>
void PublicKeyCipher::Cipher(const FunctionCallbackInfo<Value>& args) {
MarkPopErrorOnReturn mark_pop_error_on_return;
Environment* env = Environment::GetCurrent(args);
unsigned int offset = 0;
// TODO(panva): Use GetPrivateKeyFromJs() for private operations, then
// remove allow_private_key_store and URL handling from
// GetPublicOrPrivateKeyFromJs().
auto data = KeyObjectData::GetPublicOrPrivateKeyFromJs(
args, &offset, operation == PublicKeyCipher::kPrivate);
if (!data) return;
const auto& pkey = data.GetAsymmetricKey();
if (!pkey) return;
ArrayBufferOrViewContents<unsigned char> buf(args[offset]);
if (!buf.CheckSizeInt32()) [[unlikely]] {
return THROW_ERR_OUT_OF_RANGE(env, "buffer is too long");
}
uint32_t padding;
if (!args[offset + 1]->Uint32Value(env->context()).To(&padding)) return;
if (cipher == ncrypto::Cipher::decrypt &&
operation == PublicKeyCipher::kPrivate && padding == RSA_PKCS1_PADDING) {
EVPKeyCtxPointer ctx = pkey.newCtx();
CHECK(ctx);
if (!ctx.initForDecrypt()) {
return ThrowCryptoError(env, ERR_get_error());
}
// RSA implicit rejection here is not supported by BoringSSL.
if (!ctx.setRsaImplicitRejection()) [[unlikely]] {
return THROW_ERR_INVALID_ARG_VALUE(
env,
"RSA_PKCS1_PADDING is no longer supported for private decryption");
}
}
Digest digest;
if (args[offset + 2]->IsString()) {
Utf8Value oaep_str(env->isolate(), args[offset + 2]);
digest = Digest::FromName(*oaep_str);
if (!digest) return THROW_ERR_OSSL_EVP_INVALID_DIGEST(env);
}
ArrayBufferOrViewContents<unsigned char> oaep_label(
!args[offset + 3]->IsUndefined() ? args[offset + 3] : Local<Value>());
if (!oaep_label.CheckSizeInt32()) [[unlikely]] {
return THROW_ERR_OUT_OF_RANGE(env, "oaepLabel is too big");
}
Digest mgf1_digest;
if (args[offset + 4]->IsString()) {
Utf8Value mgf1_str(env->isolate(), args[offset + 4]);
mgf1_digest = Digest::FromName(*mgf1_str);
if (!mgf1_digest) return THROW_ERR_OSSL_EVP_INVALID_DIGEST(env);
}
std::unique_ptr<BackingStore> out;
if (!Cipher<cipher>(
env, pkey, padding, digest, mgf1_digest, oaep_label, buf, &out)) {
return ThrowCryptoError(env, ERR_get_error());
}
Local<ArrayBuffer> ab = ArrayBuffer::New(env->isolate(), std::move(out));
args.GetReturnValue().Set(
Buffer::New(env, ab, 0, ab->ByteLength()).FromMaybe(Local<Value>()));
}
} // namespace crypto
} // namespace node