Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions example/src/tests/argon2/argon2_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,69 @@ test(SUITE, 'argon2Sync: deterministic with same inputs', () => {
Buffer.from(r2).toString('hex'),
);
});

// --- Numeric parameter validation (Phase 1.1: validateUInt) ---
//
// `static_cast<uint32_t>(NaN | +/-Infinity | -1)` is undefined behavior in
// C++. The C++ layer used to do these casts naked; the validateUInt helper
// now rejects them with a descriptive error before the cast.

const baseParams = {
message: Buffer.from('password'),
nonce: Buffer.from('somesalt0000'),
parallelism: 1,
tagLength: 32,
memory: 64,
passes: 3,
};

test(SUITE, 'argon2Sync: rejects NaN parallelism', () => {
assert.throws(() => {
argon2Sync('argon2id', { ...baseParams, parallelism: NaN });
}, /parallelism.*NaN/i);
});

test(SUITE, 'argon2Sync: rejects +Infinity memory', () => {
assert.throws(() => {
argon2Sync('argon2id', { ...baseParams, memory: Infinity });
}, /memory.*infinity/i);
});

test(SUITE, 'argon2Sync: rejects -Infinity passes', () => {
assert.throws(() => {
argon2Sync('argon2id', { ...baseParams, passes: -Infinity });
}, /passes.*infinity/i);
});

test(SUITE, 'argon2Sync: rejects negative tagLength', () => {
assert.throws(() => {
argon2Sync('argon2id', { ...baseParams, tagLength: -1 });
}, /tagLength.*non-negative/i);
});

test(SUITE, 'argon2Sync: rejects fractional passes', () => {
assert.throws(() => {
argon2Sync('argon2id', { ...baseParams, passes: 3.5 });
}, /passes.*integer/i);
});

test(SUITE, 'argon2Sync: rejects out-of-range memory', () => {
// memory is uint32_t — anything beyond UINT32_MAX must be rejected.
assert.throws(() => {
argon2Sync('argon2id', { ...baseParams, memory: 2 ** 32 });
}, /memory.*out of range/i);
});

test(SUITE, 'argon2: async path also rejects NaN parallelism', () => {
return new Promise<void>((resolve, reject) => {
argon2('argon2id', { ...baseParams, parallelism: NaN }, err => {
try {
assert.isNotNull(err);
assert.match(err!.message, /parallelism.*NaN/i);
resolve();
} catch (e) {
reject(e);
}
});
});
});
45 changes: 45 additions & 0 deletions example/src/tests/cipher/cipher_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,3 +492,48 @@ test(
expect(() => decipher.final()).to.throw();
},
);

// --- getUIntOption type-safety regression (Phase 1.4) ---
//
// Ensure the AEAD `authTagLength` option is validated at the JS boundary.
// The previous implementation used `Record<string, any>` and the cryptic
// `value >>> 0 !== value` check; the typed replacement throws RangeError
// with a clear "must be a non-negative 32-bit integer" message.

test(SUITE, 'createCipheriv: rejects negative authTagLength', () => {
expect(() => {
createCipheriv('aes-256-gcm', key32, iv12, {
authTagLength: -1,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
}).to.throw(/non-negative/i);
});

test(SUITE, 'createCipheriv: rejects NaN authTagLength', () => {
expect(() => {
createCipheriv('aes-256-gcm', key32, iv12, {
authTagLength: NaN,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
}).to.throw(/non-negative/i);
});

test(SUITE, 'createCipheriv: rejects fractional authTagLength', () => {
expect(() => {
createCipheriv('aes-256-gcm', key32, iv12, {
authTagLength: 12.5,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
}).to.throw(/non-negative/i);
});

test(
SUITE,
'createCipheriv: missing authTagLength still defaults to 16',
() => {
// Sanity check that the new helper's `?? 16` default still kicks in.
expect(() => {
createCipheriv('aes-256-gcm', key32, iv12, {});
}).to.not.throw();
},
);
13 changes: 10 additions & 3 deletions packages/react-native-quick-crypto/cpp/argon2/HybridArgon2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ static std::shared_ptr<ArrayBuffer> hashImpl(const std::string& algorithm, const

auto type = parseAlgorithm(algorithm);

// Validate every numeric parameter before the cast. The previous code did
// `static_cast<uint32_t>(parallelism)` etc. naked, which is undefined
// behavior for NaN, +/-Infinity, or negative input — see audit Phase 1.1.
uint32_t parallelismU = validateUInt<uint32_t>(parallelism, "Argon2 parallelism");
size_t tagLengthU = validateUInt<size_t>(tagLength, "Argon2 tagLength");
uint32_t memoryU = validateUInt<uint32_t>(memory, "Argon2 memory");
uint32_t passesU = validateUInt<uint32_t>(passes, "Argon2 passes");
uint32_t versionU = validateUInt<uint32_t>(version, "Argon2 version");

ncrypto::Buffer<const char> passBuf{message->size() > 0 ? reinterpret_cast<const char*>(message->data()) : "", message->size()};

ncrypto::Buffer<const unsigned char> saltBuf{nonce->size() > 0 ? reinterpret_cast<const unsigned char*>(nonce->data())
Expand All @@ -46,9 +55,7 @@ static std::shared_ptr<ArrayBuffer> hashImpl(const std::string& algorithm, const
adBuf = {reinterpret_cast<const unsigned char*>(associatedData.value()->data()), associatedData.value()->size()};
}

auto result =
ncrypto::argon2(passBuf, saltBuf, static_cast<uint32_t>(parallelism), static_cast<size_t>(tagLength), static_cast<uint32_t>(memory),
static_cast<uint32_t>(passes), static_cast<uint32_t>(version), secretBuf, adBuf, type);
auto result = ncrypto::argon2(passBuf, saltBuf, parallelismU, tagLengthU, memoryU, passesU, versionU, secretBuf, adBuf, type);

if (!result) {
unsigned long err = ERR_peek_last_error();
Expand Down
20 changes: 10 additions & 10 deletions packages/react-native-quick-crypto/cpp/cipher/CCMCipher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ void CCMCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::s
size_t iv_len = native_iv->size();

// Set the IV length using CCM-specific control
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_IVLEN, iv_len, nullptr) != 1) {
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_CCM_SET_IVLEN, iv_len, nullptr) != 1) {
unsigned long err = ERR_get_error();
char err_buf[256];
ERR_error_string_n(err, err_buf, sizeof(err_buf));
Expand All @@ -31,7 +31,7 @@ void CCMCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::s

// Set the expected/output tag length using CCM-specific control.
// auth_tag_len should have been defaulted or set via setArgs in the base init.
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_TAG, auth_tag_len, nullptr) != 1) {
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_CCM_SET_TAG, auth_tag_len, nullptr) != 1) {
unsigned long err = ERR_get_error();
char err_buf[256];
ERR_error_string_n(err, err_buf, sizeof(err_buf));
Expand All @@ -44,7 +44,7 @@ void CCMCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::s
const unsigned char* iv_ptr = reinterpret_cast<const unsigned char*>(native_iv->data());

// The last argument (is_cipher) should be consistent with the initial setup call.
if (EVP_CipherInit_ex(ctx, nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
if (EVP_CipherInit_ex(ctx.get(), nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
unsigned long err = ERR_get_error();
char err_buf[256];
ERR_error_string_n(err, err_buf, sizeof(err_buf));
Expand All @@ -66,7 +66,7 @@ std::shared_ptr<ArrayBuffer> CCMCipher::update(const std::shared_ptr<ArrayBuffer
maybePassAuthTagToOpenSSL();
}

int block_size = EVP_CIPHER_CTX_block_size(ctx);
int block_size = EVP_CIPHER_CTX_block_size(ctx.get());
if (block_size <= 0) {
throw std::runtime_error("Invalid block size in update");
}
Expand All @@ -79,7 +79,7 @@ std::shared_ptr<ArrayBuffer> CCMCipher::update(const std::shared_ptr<ArrayBuffer
const uint8_t* in = reinterpret_cast<const uint8_t*>(native_data->data());

int actual_out_len = 0;
int ret = EVP_CipherUpdate(ctx, out_buf.get(), &actual_out_len, in, in_len);
int ret = EVP_CipherUpdate(ctx.get(), out_buf.get(), &actual_out_len, in, in_len);

if (!is_cipher) {
// Decryption: Check for tag verification failure
Expand Down Expand Up @@ -115,14 +115,14 @@ std::shared_ptr<ArrayBuffer> CCMCipher::final() {
}

// Proceed only for encryption
int block_size = EVP_CIPHER_CTX_block_size(ctx);
int block_size = EVP_CIPHER_CTX_block_size(ctx.get());
if (block_size <= 0) {
throw std::runtime_error("Invalid block size");
}
auto out_buf = std::make_unique<unsigned char[]>(block_size);
int out_len = 0;

if (!EVP_CipherFinal_ex(ctx, out_buf.get(), &out_len)) {
if (!EVP_CipherFinal_ex(ctx.get(), out_buf.get(), &out_len)) {
unsigned long err = ERR_get_error();
char err_buf[256];
ERR_error_string_n(err, err_buf, sizeof(err_buf));
Expand All @@ -133,7 +133,7 @@ std::shared_ptr<ArrayBuffer> CCMCipher::final() {
auth_tag_len = sizeof(auth_tag);
}

if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_GET_TAG, auth_tag_len, auth_tag) != 1) {
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_CCM_GET_TAG, auth_tag_len, auth_tag) != 1) {
unsigned long err = ERR_get_error();
char err_buf[256];
ERR_error_string_n(err, err_buf, sizeof(err_buf));
Expand Down Expand Up @@ -179,7 +179,7 @@ bool CCMCipher::setAAD(const std::shared_ptr<ArrayBuffer>& data, std::optional<d
// BUT the wiki says "(only needed if AAD is passed)". Let's skip if decrypting and AAD length is 0.
bool should_set_total_length = is_cipher || aad_len > 0;
if (should_set_total_length) {
if (EVP_CipherUpdate(ctx, nullptr, &out_len, nullptr, data_len) != 1) {
if (EVP_CipherUpdate(ctx.get(), nullptr, &out_len, nullptr, data_len) != 1) {
unsigned long err = ERR_get_error();
char err_buf[256];
ERR_error_string_n(err, err_buf, sizeof(err_buf));
Expand All @@ -190,7 +190,7 @@ bool CCMCipher::setAAD(const std::shared_ptr<ArrayBuffer>& data, std::optional<d
// 2. Process AAD Data
// Per OpenSSL CCM decryption examples, this MUST be called even if aad_len is 0.
// Pass nullptr as the output buffer, the AAD data pointer, and its length.
if (EVP_CipherUpdate(ctx, nullptr, &out_len, native_aad->data(), aad_len) != 1) {
if (EVP_CipherUpdate(ctx.get(), nullptr, &out_len, native_aad->data(), aad_len) != 1) {
unsigned long err = ERR_get_error();
char err_buf[256];
ERR_error_string_n(err, err_buf, sizeof(err_buf));
Expand Down
6 changes: 2 additions & 4 deletions packages/react-native-quick-crypto/cpp/cipher/CCMCipher.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ namespace margelo::nitro::crypto {
class CCMCipher : public HybridCipher {
public:
CCMCipher() : HybridObject(TAG) {}
~CCMCipher() {
// Let parent destructor free the context
ctx = nullptr;
}
// Destructor defaulted: HybridCipher's unique_ptr ctx frees itself.
~CCMCipher() override = default;

void init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) override;
std::shared_ptr<ArrayBuffer> update(const std::shared_ptr<ArrayBuffer>& data) override;
Expand Down
21 changes: 8 additions & 13 deletions packages/react-native-quick-crypto/cpp/cipher/ChaCha20Cipher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@
namespace margelo::nitro::crypto {

void ChaCha20Cipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) {
// Clean up any existing context
if (ctx) {
EVP_CIPHER_CTX_free(ctx);
ctx = nullptr;
}
// Resetting the unique_ptr frees any previous context.
ctx.reset();

// Get ChaCha20 cipher implementation
const EVP_CIPHER* cipher = EVP_chacha20();
Expand All @@ -20,18 +17,17 @@ void ChaCha20Cipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const s
}

// Create a new context
ctx = EVP_CIPHER_CTX_new();
ctx.reset(EVP_CIPHER_CTX_new());
if (!ctx) {
throw std::runtime_error("Failed to create cipher context");
}

// Initialize the encryption/decryption operation
if (EVP_CipherInit_ex(ctx, cipher, nullptr, nullptr, nullptr, is_cipher) != 1) {
if (EVP_CipherInit_ex(ctx.get(), cipher, nullptr, nullptr, nullptr, is_cipher) != 1) {
unsigned long err = ERR_get_error();
char err_buf[256];
ERR_error_string_n(err, err_buf, sizeof(err_buf));
EVP_CIPHER_CTX_free(ctx);
ctx = nullptr;
ctx.reset();
throw std::runtime_error("ChaCha20Cipher: Failed initial CipherInit setup: " + std::string(err_buf));
}

Expand All @@ -52,12 +48,11 @@ void ChaCha20Cipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const s
const unsigned char* key_ptr = reinterpret_cast<const unsigned char*>(native_key->data());
const unsigned char* iv_ptr = reinterpret_cast<const unsigned char*>(native_iv->data());

if (EVP_CipherInit_ex(ctx, nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
if (EVP_CipherInit_ex(ctx.get(), nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
unsigned long err = ERR_get_error();
char err_buf[256];
ERR_error_string_n(err, err_buf, sizeof(err_buf));
EVP_CIPHER_CTX_free(ctx);
ctx = nullptr;
ctx.reset();
throw std::runtime_error("ChaCha20Cipher: Failed to set key/IV: " + std::string(err_buf));
}
}
Expand All @@ -76,7 +71,7 @@ std::shared_ptr<ArrayBuffer> ChaCha20Cipher::update(const std::shared_ptr<ArrayB
uint8_t* out = new uint8_t[out_len];

// Perform the cipher update operation
if (EVP_CipherUpdate(ctx, out, &out_len, native_data->data(), in_len) != 1) {
if (EVP_CipherUpdate(ctx.get(), out, &out_len, native_data->data(), in_len) != 1) {
delete[] out;
unsigned long err = ERR_get_error();
char err_buf[256];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ namespace margelo::nitro::crypto {
class ChaCha20Cipher : public HybridCipher {
public:
ChaCha20Cipher() : HybridObject(TAG) {}
~ChaCha20Cipher() {
// Let parent destructor free the context
ctx = nullptr;
}
// Destructor defaulted: HybridCipher's unique_ptr ctx frees itself.
~ChaCha20Cipher() override = default;

void init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) override;
std::shared_ptr<ArrayBuffer> update(const std::shared_ptr<ArrayBuffer>& data) override;
Expand Down
Loading
Loading