Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ciphers #573

Draft
wants to merge 37 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
824fd40
feat: add ciphers
boorad Jan 3, 2025
e329d7d
TS/Nitro work
boorad Jan 3, 2025
d95faf5
Nitro spec format correction
boorad Jan 7, 2025
c461fd7
C++ compiling and couple tests passing
boorad Jan 7, 2025
50c6b5f
lint & format
boorad Jan 7, 2025
8bf7c4a
bump checkbox component
boorad Jan 7, 2025
99e5563
android build
boorad Jan 7, 2025
7770dcb
ts
boorad Jan 14, 2025
eb5cc32
add getSupportedCiphers
boorad Jan 18, 2025
384777f
lint
boorad Jan 18, 2025
a41e71b
stay close to Node API
boorad Jan 18, 2025
aa01057
cipher init works, js internals closer to recent node updates
boorad Jan 18, 2025
8f7d4ee
more Node API compliance
boorad Jan 18, 2025
21f5bbe
C++ work on update/final, added tests
boorad Jan 22, 2025
a44e4df
troubleshooting
boorad Jan 22, 2025
e933da0
stack -> heap buffer for final()
boorad Jan 23, 2025
1fa6642
wip
boorad Jan 25, 2025
2fb9623
remove CipherType
boorad Jan 26, 2025
031aa21
simplify decoding, 108 pass, 23 fail
boorad Jan 27, 2025
bd63b6b
lint
boorad Jan 27, 2025
437d250
remove copy(), prep for different cipher modes
boorad Jan 28, 2025
1afed70
async not needed on tests
boorad Jan 29, 2025
5f316c4
auth mode supporting methods
boorad Feb 1, 2025
dcd7c69
wip
boorad Feb 3, 2025
eaa0ef1
don't use CipherArgs in implementation
boorad Feb 4, 2025
6e5fa42
wip - GCM & final()
boorad Feb 4, 2025
b438372
chore: rebase hash PR
boorad Feb 5, 2025
c096a36
update closer to 0.x cpp code
boorad Feb 8, 2025
706dcd3
OCB working
boorad Feb 9, 2025
a3ee7a3
owned ArrayBuffers, memory safety, and mode conditionals
boorad Feb 9, 2025
48d71fd
separate init functions for the problematic modes
boorad Feb 10, 2025
46055ff
set up HybridCipher to be a base class
boorad Feb 10, 2025
de153c3
more polymorphism
boorad Feb 11, 2025
5e0611d
AI fucking sucks
boorad Feb 11, 2025
738ce07
inheritance working, CCM still failing in update()
boorad Feb 13, 2025
3f12070
weekend work
boorad Feb 17, 2025
5c229ba
rebase
boorad Mar 2, 2025
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
Prev Previous commit
Next Next commit
more polymorphism
boorad committed Mar 1, 2025
commit de153c38225635255cacbe49cfc9b04bfd132e0e
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -186,4 +186,4 @@ tsconfig.tsbuildinfo

# development stuffs
*scratch*

.*rules*
127 changes: 127 additions & 0 deletions packages/react-native-quick-crypto/cpp/cipher/CCMCipher.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#include "CCMCipher.hpp"
#include <stdexcept>

// bool CCMCipher::setAuthTag(const uint8_t* tag, int tag_len) {
// if (!tag || tag_len < 4 || tag_len > 16) {
// return false;
// }

// if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_TAG, tag_len, const_cast<uint8_t*>(tag))) {
// return false;
// }

// auth_tag_state = kAuthTagPassedToOpenSSL;
// return true;
// }

namespace margelo::nitro::crypto {

// CCMCipher::CCMCipher(const EVP_CIPHER* cipher,
// bool encrypt,
// const uint8_t* key,
// const uint8_t* iv,
// int iv_len)
// : HybridCipher() {

// // Initialize EVP context
// ctx = EVP_CIPHER_CTX_new();
// if (!ctx) {
// throw std::runtime_error("Failed to create cipher context");
// }

// // Initialize with null key and IV first for CCM mode
// if (EVP_CipherInit_ex2(ctx, cipher, nullptr, nullptr, encrypt ? 1 : 0, nullptr) != 1) {
// EVP_CIPHER_CTX_free(ctx);
// throw std::runtime_error("Failed to initialize cipher");
// }

// // Now set the key
// if (EVP_CipherInit_ex2(ctx, nullptr, key, nullptr, -1, nullptr) != 1) {
// EVP_CIPHER_CTX_free(ctx);
// throw std::runtime_error("Failed to set key");
// }

// // Set IV length for CCM
// if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_IVLEN, iv_len, nullptr) != 1) {
// EVP_CIPHER_CTX_free(ctx);
// throw std::runtime_error("Failed to set IV length");
// }

// // Set IV
// if (EVP_CipherInit_ex2(ctx, nullptr, nullptr, iv, -1, nullptr) != 1) {
// EVP_CIPHER_CTX_free(ctx);
// throw std::runtime_error("Failed to set IV");
// }

// is_cipher = encrypt;
// }

// bool CCMCipher::initializeImpl() {
// // CCM requires message length to be known in advance
// has_aad = false;
// auth_tag_len = 16; // Default CCM tag length

// // Set the tag length for CCM mode
// if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_TAG, auth_tag_len, nullptr)) {
// throw std::runtime_error("Failed to set CCM tag length");
// }

// return true;
// }

// bool CCMCipher::updateImpl(const uint8_t* data, int data_len, uint8_t* out, int* out_len) {
// if (!has_aad) {
// throw std::runtime_error("setAAD() must be called before update() in CCM mode");
// }

// // CCM mode requires one-shot encryption/decryption
// return EVP_CipherUpdate(ctx, out, out_len, data, data_len) == 1;
// }

// bool CCMCipher::finalImpl(uint8_t* out, int* out_len) {
// if (!EVP_CipherFinal_ex(ctx, out, out_len)) {
// return false;
// }

// if (is_cipher) {
// // For CCM mode, we need to get the tag after finalization
// std::memset(auth_tag, 0, EVP_GCM_TLS_TAG_LEN);
// if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_GET_TAG, auth_tag_len, auth_tag)) {
// throw std::runtime_error("CCM tag retrieval failed (length: " + std::to_string(auth_tag_len) + ")");
// }
// auth_tag_state = kAuthTagKnown;
// }

// return true;
// }

// bool CCMCipher::setAADImpl(const uint8_t* aad, int aad_len, int plaintext_len) {
// if (!checkMessageLength(plaintext_len)) {
// return false;
// }

// // For CCM mode, we must set the total plaintext length before processing AAD
// if (!EVP_CipherUpdate(ctx, nullptr, nullptr, nullptr, plaintext_len)) {
// return false;
// }

// // Process AAD if present
// if (aad_len > 0 && aad != nullptr) {
// int temp_len;
// if (!EVP_CipherUpdate(ctx, nullptr, &temp_len, aad, aad_len)) {
// return false;
// }
// }

// has_aad = true;
// return true;
// }

// bool CCMCipher::checkMessageLength(int message_len) {
// if (message_len > kMaxMessageSize) {
// throw std::runtime_error("Cannot create larger than " + std::to_string(kMaxMessageSize) + " bytes");
// }
// return true;
// }

} // namespace margelo::nitro::crypto
14 changes: 14 additions & 0 deletions packages/react-native-quick-crypto/cpp/cipher/CCMCipher.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once

#include "HybridCipher.hpp"

namespace margelo::nitro::crypto {

class CCMCipher : public HybridCipher {
public:

private:
static constexpr int kMaxMessageSize = ((1ull << 32) - 1) * 8;
};

} // namespace margelo::nitro::crypto
20 changes: 9 additions & 11 deletions packages/react-native-quick-crypto/cpp/cipher/HybridCipher.hpp
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
#pragma once

#include <memory>
#include <NitroModules/ArrayBuffer.hpp>
#include <openssl/evp.h>
#include <optional>
#include <string>
#include <vector>
#include <openssl/evp.h>

#include "HybridCipherSpec.hpp"
#include "CipherArgs.hpp"

namespace margelo::nitro::crypto {

using namespace facebook;

class HybridCipher : public HybridCipherSpec {
protected:
// Protected enums for state management
enum CipherKind { kCipher, kDecipher };
enum UpdateResult { kSuccess, kErrorMessageSize, kErrorState };
enum AuthTagState { kAuthTagUnknown, kAuthTagKnown, kAuthTagPassedToOpenSSL };

public:
HybridCipher() : HybridObject(TAG) {}
~HybridCipher();
@@ -60,6 +52,12 @@ class HybridCipher : public HybridCipherSpec {
std::vector<std::string>
getSupportedCiphers() override;

protected:
// Protected enums for state management
enum CipherKind { kCipher, kDecipher };
enum UpdateResult { kSuccess, kErrorMessageSize, kErrorState };
enum AuthTagState { kAuthTagUnknown, kAuthTagKnown, kAuthTagPassedToOpenSSL };

private:
// Methods
void init(
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#pragma once

#include <string>
#include <memory>
#include <openssl/evp.h>

#include "HybridCipherFactorySpec.hpp"
#include "CCMCipher.hpp"

namespace margelo::nitro::crypto {

using namespace facebook;

class HybridCipherFactory : public HybridCipherFactorySpec {
public:
HybridCipherFactory() : HybridObject(TAG) {}
~HybridCipherFactory() = default;

public:
// Factory method exposed to JS
inline std::shared_ptr<HybridCipherSpec> createCipher(const CipherArgs& args) {
// Create a temporary cipher context to determine the mode
EVP_CIPHER* cipher = EVP_CIPHER_fetch(nullptr, args.cipherType.c_str(), nullptr);
if (!cipher) {
throw std::runtime_error("Invalid cipher type: " + args.cipherType);
}

int mode = EVP_CIPHER_get_mode(cipher);
EVP_CIPHER_free(cipher);

// Create the appropriate cipher instance based on mode
switch (mode) {
case EVP_CIPH_CCM_MODE: {
auto ccm = std::make_shared<CCMCipher>();
ccm->setArgs(args);
return ccm;
}
// Add other modes as they are implemented
default: {
// For all other modes, use the base HybridCipher
auto base = std::make_shared<HybridCipher>();
base->setArgs(args);
return base;
}
}
}
};

} // namespace margelo::nitro::crypto
1 change: 1 addition & 0 deletions packages/react-native-quick-crypto/nitro.json
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
},
"autolinking": {
"Cipher": { "cpp": "HybridCipher" },
"CipherFactory": { "cpp": "HybridCipherFactory" },
"EdKeyPair": { "cpp": "HybridEdKeyPair" },
"Hash": { "cpp": "HybridHash" },
"Hmac": { "cpp": "HybridHmac" },
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ target_sources(
../nitrogen/generated/android/QuickCryptoOnLoad.cpp
# Shared Nitrogen C++ sources
../nitrogen/generated/shared/c++/HybridCipherSpec.cpp
../nitrogen/generated/shared/c++/HybridCipherFactorySpec.cpp
../nitrogen/generated/shared/c++/HybridEdKeyPairSpec.cpp
../nitrogen/generated/shared/c++/HybridHashSpec.cpp
../nitrogen/generated/shared/c++/HybridHmacSpec.cpp
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
#include <NitroModules/HybridObjectRegistry.hpp>

#include "HybridCipher.hpp"
#include "HybridCipherFactory.hpp"
#include "HybridEdKeyPair.hpp"
#include "HybridHash.hpp"
#include "HybridHmac.hpp"
@@ -43,6 +44,15 @@ int initialize(JavaVM* vm) {
return std::make_shared<HybridCipher>();
}
);
HybridObjectRegistry::registerHybridObjectConstructor(
"CipherFactory",
[]() -> std::shared_ptr<HybridObject> {
static_assert(std::is_default_constructible_v<HybridCipherFactory>,
"The HybridObject \"HybridCipherFactory\" is not default-constructible! "
"Create a public constructor that takes zero arguments to be able to autolink this HybridObject.");
return std::make_shared<HybridCipherFactory>();
}
);
HybridObjectRegistry::registerHybridObjectConstructor(
"EdKeyPair",
[]() -> std::shared_ptr<HybridObject> {
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@
#import <type_traits>

#include "HybridCipher.hpp"
#include "HybridCipherFactory.hpp"
#include "HybridEdKeyPair.hpp"
#include "HybridHash.hpp"
#include "HybridHmac.hpp"
@@ -35,6 +36,15 @@ + (void) load {
return std::make_shared<HybridCipher>();
}
);
HybridObjectRegistry::registerHybridObjectConstructor(
"CipherFactory",
[]() -> std::shared_ptr<HybridObject> {
static_assert(std::is_default_constructible_v<HybridCipherFactory>,
"The HybridObject \"HybridCipherFactory\" is not default-constructible! "
"Create a public constructor that takes zero arguments to be able to autolink this HybridObject.");
return std::make_shared<HybridCipherFactory>();
}
);
HybridObjectRegistry::registerHybridObjectConstructor(
"EdKeyPair",
[]() -> std::shared_ptr<HybridObject> {
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
///
/// HybridCipherFactorySpec.cpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © 2025 Marc Rousavy @ Margelo
///

#include "HybridCipherFactorySpec.hpp"

namespace margelo::nitro::crypto {

void HybridCipherFactorySpec::loadHybridMethods() {
// load base methods/properties
HybridObject::loadHybridMethods();
// load custom methods/properties
registerHybrids(this, [](Prototype& prototype) {
prototype.registerHybridMethod("createCipher", &HybridCipherFactorySpec::createCipher);
});
}

} // namespace margelo::nitro::crypto
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
///
/// HybridCipherFactorySpec.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © 2025 Marc Rousavy @ Margelo
///

#pragma once

#if __has_include(<NitroModules/HybridObject.hpp>)
#include <NitroModules/HybridObject.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif

// Forward declaration of `HybridCipherSpec` to properly resolve imports.
namespace margelo::nitro::crypto { class HybridCipherSpec; }
// Forward declaration of `CipherArgs` to properly resolve imports.
namespace margelo::nitro::crypto { struct CipherArgs; }

#include <memory>
#include "HybridCipherSpec.hpp"
#include "CipherArgs.hpp"

namespace margelo::nitro::crypto {

using namespace margelo::nitro;

/**
* An abstract base class for `CipherFactory`
* Inherit this class to create instances of `HybridCipherFactorySpec` in C++.
* You must explicitly call `HybridObject`'s constructor yourself, because it is virtual.
* @example
* ```cpp
* class HybridCipherFactory: public HybridCipherFactorySpec {
* public:
* HybridCipherFactory(...): HybridObject(TAG) { ... }
* // ...
* };
* ```
*/
class HybridCipherFactorySpec: public virtual HybridObject {
public:
// Constructor
explicit HybridCipherFactorySpec(): HybridObject(TAG) { }

// Destructor
virtual ~HybridCipherFactorySpec() { }

public:
// Properties


public:
// Methods
virtual std::shared_ptr<margelo::nitro::crypto::HybridCipherSpec> createCipher(const CipherArgs& args) = 0;

protected:
// Hybrid Setup
void loadHybridMethods() override;

protected:
// Tag for logging
static constexpr auto TAG = "CipherFactory";
};

} // namespace margelo::nitro::crypto
11 changes: 8 additions & 3 deletions packages/react-native-quick-crypto/src/cipher.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,10 @@ import type {
CipherOCBOptions,
CipherOCBTypes,
} from 'crypto'; // @types/node
import type { Cipher as NativeCipher } from './specs/cipher.nitro';
import type {
Cipher as NativeCipher,
CipherFactory,
} from './specs/cipher.nitro';
import { ab2str, binaryLikeToArrayBuffer } from './utils';
import type { BinaryLike, BinaryLikeNode, Encoding } from './utils';
import {
@@ -50,12 +53,14 @@ class CipherCommon extends Stream.Transform {
options = {},
}: CipherArgs) {
super(options);
this.native = NitroModules.createHybridObject<NativeCipher>('Cipher');
const authTagLen: number =
getUIntOption(options, 'authTagLength') !== -1
? getUIntOption(options, 'authTagLength')
: 16; // defaults to 16 bytes
this.native.setArgs({

const factory =
NitroModules.createHybridObject<CipherFactory>('CipherFactory');
this.native = factory.createCipher({
isCipher,
cipherType,
cipherKey: binaryLikeToArrayBuffer(cipherKey),
4 changes: 4 additions & 0 deletions packages/react-native-quick-crypto/src/specs/cipher.nitro.ts
Original file line number Diff line number Diff line change
@@ -18,3 +18,7 @@ export interface Cipher extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
getAuthTag(): ArrayBuffer;
getSupportedCiphers(): string[];
}

export interface CipherFactory extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
createCipher(args: CipherArgs): Cipher;
}