diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28163b4..5c5754f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,9 +4,11 @@ on: push: branches: - main + - feat-add-pro-backend-fns pull_request: branches: - main + - feat-add-pro-backend-fns concurrency: group: ${{ github.workflow }} @@ -19,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-2022, macos-13, macos-14, ubuntu-22.04] + os: [windows-2022, macos-13, macos-14, ubuntu-24.04] env: SIGNAL_ENV: production GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -29,16 +31,21 @@ jobs: - name: Checkout git repo uses: actions/checkout@v4 with: - submodules: 'recursive' + submodules: "recursive" + + - name: Install correct clang versions + if: runner.os == 'Linux' + run: sudo apt update && sudo apt search clang && sudo apt install clang-format-19 #clang-tidy-19 + shell: bash - name: Install node uses: actions/setup-node@v3 with: - node-version-file: '.nvmrc' + node-version-file: ".nvmrc" - uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version: "3.11" - name: Add msbuild to PATH uses: microsoft/setup-msbuild@v1.3.1 @@ -48,7 +55,15 @@ jobs: shell: bash run: yarn update_version - - name: build libsession-util-nodejs shell: bash run: yarn install --frozen-lockfile + + - name: Check formatting + if: runner.os == 'Linux' + run: clang-format-19 --dry-run --Werror src/*.cpp include/*.hpp src/**/*.cpp include/**/*.hpp + shell: bash + + # - name: Run clang-tidy + # run: clang-tidy-19 -p build src/*.cpp include/*.hpp src/**/*.cpp include/**/*.hpp + # shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e53f69..6a4aa83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,18 +37,17 @@ set(ENABLE_ONIONREQ OFF) # as it is not part of the archive. We actually don't care about it on session-desktop set(SUBMODULE_CHECK OFF) -file(GLOB SOURCE_FILES src/*.cpp src/groups/*.cpp src/multi_encrypt/*.cpp) +file(GLOB SOURCE_FILES src/*.cpp src/groups/*.cpp src/encrypt_decrypt/*.cpp src/pro/*.cpp src/meta/*.cpp ) add_subdirectory(libsession-util) - if(MSVC) # Windows is horrible add_compile_definitions(NOMINMAX) endif() add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC}) -target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_JS_INC} "include/" "node_modules/node-addon-api" "../../node_modules/node-addon-api" "node_modules/node-api-headers/include" "../../node_modules/node-api-headers/include") +target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_JS_INC} "include/" "${CMAKE_CURRENT_SOURCE_DIR}/node_modules/node-addon-api" "${CMAKE_CURRENT_SOURCE_DIR}/node_modules" "${CMAKE_CURRENT_SOURCE_DIR}/node_modules/node-api-headers/include") set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") target_link_libraries(${PROJECT_NAME} PRIVATE ${CMAKE_JS_LIB} ${LIBSESSION_STATIC_BUNDLE_LIBS}) diff --git a/include/base_config.hpp b/include/base_config.hpp index 114d864..38985ba 100644 --- a/include/base_config.hpp +++ b/include/base_config.hpp @@ -6,13 +6,10 @@ #include #include #include -#include #include #include #include "session/config/base.hpp" -#include "session/logging.hpp" -#include "session/types.hpp" #include "utilities.hpp" namespace session::nodeapi { diff --git a/include/encrypt_decrypt/encrypt_decrypt.hpp b/include/encrypt_decrypt/encrypt_decrypt.hpp new file mode 100644 index 0000000..7b3430c --- /dev/null +++ b/include/encrypt_decrypt/encrypt_decrypt.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include + +#include "meta/meta_base_wrapper.hpp" + +namespace session::nodeapi { + +class MultiEncryptWrapper : public Napi::ObjectWrap { + public: + MultiEncryptWrapper(const Napi::CallbackInfo& info) : + Napi::ObjectWrap{info} { + throw std::invalid_argument( + "MultiEncryptWrapper is static and doesn't need to be constructed"); + } + + static void Init(Napi::Env env, Napi::Object exports) { + MetaBaseWrapper::NoBaseClassInitHelper( + env, + exports, + "MultiEncryptWrapperNode", + { + StaticMethod<&MultiEncryptWrapper::multiEncrypt>( + "multiEncrypt", + static_cast( + napi_writable | napi_configurable)), + StaticMethod<&MultiEncryptWrapper::multiDecryptEd25519>( + "multiDecryptEd25519", + static_cast( + napi_writable | napi_configurable)), + // Attachments encrypt/decrypt + StaticMethod<&MultiEncryptWrapper::attachmentDecrypt>( + "attachmentDecrypt", + static_cast( + napi_writable | napi_configurable)), + StaticMethod<&MultiEncryptWrapper::attachmentEncrypt>( + "attachmentEncrypt", + static_cast( + napi_writable | napi_configurable)), + + // Destination encrypt + StaticMethod<&MultiEncryptWrapper::encryptFor1o1>( + "encryptFor1o1", + static_cast( + napi_writable | napi_configurable)), + + StaticMethod<&MultiEncryptWrapper::encryptForCommunity>( + "encryptForCommunity", + static_cast( + napi_writable | napi_configurable)), + StaticMethod<&MultiEncryptWrapper::encryptForCommunityInbox>( + "encryptForCommunityInbox", + static_cast( + napi_writable | napi_configurable)), + StaticMethod<&MultiEncryptWrapper::encryptForGroup>( + "encryptForGroup", + static_cast( + napi_writable | napi_configurable)), + + // Destination decrypt + StaticMethod<&MultiEncryptWrapper::decryptForCommunity>( + "decryptForCommunity", + static_cast( + napi_writable | napi_configurable)), + StaticMethod<&MultiEncryptWrapper::decryptFor1o1>( + "decryptFor1o1", + static_cast( + napi_writable | napi_configurable)), + StaticMethod<&MultiEncryptWrapper::decryptForGroup>( + "decryptForGroup", + static_cast( + napi_writable | napi_configurable)), + }); + } + + private: + static Napi::Value multiEncrypt(const Napi::CallbackInfo& info); + static Napi::Value multiDecryptEd25519(const Napi::CallbackInfo& info); + + /** + * =========================================== + * =========== ATTACHMENTS CALLS ============= + * =========================================== + */ + + static Napi::Value attachmentEncrypt(const Napi::CallbackInfo& info); + static Napi::Value attachmentDecrypt(const Napi::CallbackInfo& info); + + /** + * =========================================== + * ============= ENCRYPT CALLS =============== + * =========================================== + */ + + static Napi::Value encryptFor1o1(const Napi::CallbackInfo& info); + static Napi::Value encryptForCommunityInbox(const Napi::CallbackInfo& info); + static Napi::Value encryptForCommunity(const Napi::CallbackInfo& info); + static Napi::Value encryptForGroup(const Napi::CallbackInfo& info); + /** + * =========================================== + * ============= DECRYPT CALLS =============== + * =========================================== + */ + + static Napi::Value decryptForCommunity(const Napi::CallbackInfo& info); + static Napi::Value decryptFor1o1(const Napi::CallbackInfo& info); + static Napi::Value decryptForGroup(const Napi::CallbackInfo& info); +}; +}; // namespace session::nodeapi diff --git a/include/groups/meta_group.hpp b/include/groups/meta_group.hpp index 18c53bf..97809f1 100644 --- a/include/groups/meta_group.hpp +++ b/include/groups/meta_group.hpp @@ -1,6 +1,7 @@ #pragma once #include + #include #include "session/config/groups/info.hpp" @@ -35,7 +36,10 @@ class MetaGroup { shared_ptr keys, std::vector edGroupPubKey, std::optional> edGroupSecKey) : - info{info}, members{members}, keys{keys}, edGroupPubKey{oxenc::to_hex(edGroupPubKey.begin(), edGroupPubKey.end())} { + info{info}, + members{members}, + keys{keys}, + edGroupPubKey{oxenc::to_hex(edGroupPubKey.begin(), edGroupPubKey.end())} { if (edGroupSecKey.has_value()) { this->edGroupSecKey = oxenc::to_hex(edGroupSecKey->begin(), edGroupSecKey->end()); diff --git a/include/groups/meta_group_wrapper.hpp b/include/groups/meta_group_wrapper.hpp index dead88e..88c2ed2 100644 --- a/include/groups/meta_group_wrapper.hpp +++ b/include/groups/meta_group_wrapper.hpp @@ -80,6 +80,7 @@ class MetaGroupWrapper : public Napi::ObjectWrap { Napi::Value keysNeedsRekey(const Napi::CallbackInfo& info); Napi::Value keyRekey(const Napi::CallbackInfo& info); Napi::Value keyGetAll(const Napi::CallbackInfo& info); + Napi::Value keyGetEncryptionKeyHex(const Napi::CallbackInfo& info); Napi::Value loadKeyMessage(const Napi::CallbackInfo& info); Napi::Value keyGetCurrentGen(const Napi::CallbackInfo& info); Napi::Value activeHashes(const Napi::CallbackInfo& info); diff --git a/include/meta/meta_base_wrapper.hpp b/include/meta/meta_base_wrapper.hpp index eca139c..c2396b3 100644 --- a/include/meta/meta_base_wrapper.hpp +++ b/include/meta/meta_base_wrapper.hpp @@ -2,18 +2,18 @@ #include -#include -#include +#include #include #include "../base_config.hpp" -#include "../groups/meta_group.hpp" +#include "groups/meta_group.hpp" + namespace session::nodeapi { class MetaBaseWrapper { public: - explicit MetaBaseWrapper(){}; + explicit MetaBaseWrapper() {}; virtual ~MetaBaseWrapper() = default; @@ -35,86 +35,7 @@ class MetaBaseWrapper { } static std::unique_ptr constructGroupWrapper( - const Napi::CallbackInfo& info, const std::string& class_name) { - return wrapExceptions(info, [&] { - if (!info.IsConstructCall()) - throw std::invalid_argument{ - "You need to call the constructor with the `new` syntax"}; - - assertInfoLength(info, 1); - auto arg = info[0]; - assertIsObject(arg); - auto obj = arg.As(); - - if (obj.IsEmpty()) - throw std::invalid_argument("constructGroupWrapper received empty"); - - assertIsUInt8Array(obj.Get("userEd25519Secretkey"), "constructGroupWrapper userEd"); - auto user_ed25519_secretkey = toCppBuffer( - obj.Get("userEd25519Secretkey"), - class_name + ":constructGroupWrapper.userEd25519Secretkey"); - - assertIsUInt8Array(obj.Get("groupEd25519Pubkey"), "constructGroupWrapper groupEd"); - auto group_ed25519_pubkey = toCppBuffer( - obj.Get("groupEd25519Pubkey"), - class_name + ":constructGroupWrapper.groupEd25519Pubkey"); - - std::optional> group_ed25519_secretkey = maybeNonemptyBuffer( - obj.Get("groupEd25519Secretkey"), - class_name + ":constructGroupWrapper.groupEd25519Secretkey"); - - std::optional> dumped_meta = maybeNonemptyBuffer( - obj.Get("metaDumped"), class_name + ":constructGroupWrapper.metaDumped"); - - std::optional dumped_info; - std::optional dumped_members; - std::optional dumped_keys; - - if (dumped_meta) { - auto dumped_meta_str = to_string(*dumped_meta); - - oxenc::bt_dict_consumer combined{dumped_meta_str}; - // NB: must read in ascii-sorted order: - if (!combined.skip_until("info")) - throw std::runtime_error{"info dump not found in combined dump!"}; - dumped_info = combined.consume_string(); - - if (!combined.skip_until("keys")) - throw std::runtime_error{"keys dump not found in combined dump!"}; - dumped_keys = combined.consume_string(); - - if (!combined.skip_until("members")) - throw std::runtime_error{"members dump not found in combined dump!"}; - dumped_members = combined.consume_string(); - } - - // Note, we keep shared_ptr for those as the Keys one need a reference to Members and - // Info on its own currently. - auto info = std::make_shared( - group_ed25519_pubkey, - group_ed25519_secretkey, - (dumped_info ? std::make_optional(session::to_span(*dumped_info)) - : std::nullopt)); - - auto members = std::make_shared( - group_ed25519_pubkey, - group_ed25519_secretkey, - (dumped_members ? std::make_optional(session::to_span(*dumped_members)) - : std::nullopt)); - - auto keys = std::make_shared( - user_ed25519_secretkey, - group_ed25519_pubkey, - group_ed25519_secretkey, - (dumped_keys ? std::make_optional(session::to_span(*dumped_keys)) - : std::nullopt), - *info, - *members); - - return std::make_unique( - info, members, keys, group_ed25519_pubkey, group_ed25519_secretkey); - }); - } + const Napi::CallbackInfo& info, const std::string& class_name); }; } // namespace session::nodeapi diff --git a/include/multi_encrypt/multi_encrypt.hpp b/include/multi_encrypt/multi_encrypt.hpp deleted file mode 100644 index 3398c1e..0000000 --- a/include/multi_encrypt/multi_encrypt.hpp +++ /dev/null @@ -1,229 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -#include "../utilities.hpp" -#include "session/attachments.hpp" -#include "session/config/user_profile.hpp" -#include "session/multi_encrypt.hpp" -#include "session/random.hpp" - -namespace session::nodeapi { - -class MultiEncryptWrapper : public Napi::ObjectWrap { - public: - MultiEncryptWrapper(const Napi::CallbackInfo& info) : - Napi::ObjectWrap{info} { - throw std::invalid_argument( - "MultiEncryptWrapper is static and doesn't need to be constructed"); - } - - static void Init(Napi::Env env, Napi::Object exports) { - MetaBaseWrapper::NoBaseClassInitHelper( - env, - exports, - "MultiEncryptWrapperNode", - { - StaticMethod<&MultiEncryptWrapper::multiEncrypt>( - "multiEncrypt", - static_cast( - napi_writable | napi_configurable)), - StaticMethod<&MultiEncryptWrapper::multiDecryptEd25519>( - "multiDecryptEd25519", - static_cast( - napi_writable | napi_configurable)), - StaticMethod<&MultiEncryptWrapper::attachmentDecrypt>( - "attachmentDecrypt", - static_cast( - napi_writable | napi_configurable)), - StaticMethod<&MultiEncryptWrapper::attachmentEncrypt>( - "attachmentEncrypt", - static_cast( - napi_writable | napi_configurable)), - }); - } - - private: - static Napi::Value multiEncrypt(const Napi::CallbackInfo& info) { - return wrapResult(info, [&] { - assertInfoLength(info, 1); - assertIsObject(info[0]); - auto obj = info[0].As(); - - if (obj.IsEmpty()) - throw std::invalid_argument("multiEncrypt received empty"); - - assertIsUInt8Array(obj.Get("ed25519SecretKey"), "multiEncrypt.ed25519SecretKey"); - auto ed25519SecretKey = - toCppBuffer(obj.Get("ed25519SecretKey"), "multiEncrypt.ed25519SecretKey"); - - assertIsString(obj.Get("domain")); - auto domain = toCppString(obj.Get("domain"), "multiEncrypt.domain"); - - // handle the messages conversion - auto messagesJSValue = obj.Get("messages"); - assertIsArray(messagesJSValue, "multiEncrypt"); - auto messagesJS = messagesJSValue.As(); - std::vector> messages; - messages.reserve(messagesJS.Length()); - for (uint32_t i = 0; i < messagesJS.Length(); i++) { - auto itemValue = messagesJS.Get(i); - assertIsUInt8Array(itemValue, "multiEncrypt.itemValue.message"); - auto item = toCppBuffer(itemValue, "multiEncrypt.itemValue.message"); - messages.push_back(item); - } - - // handle the recipients conversion - auto recipientsJSValue = obj.Get("recipients"); - assertIsArray(recipientsJSValue, "multiEncrypt"); - auto recipientsJS = recipientsJSValue.As(); - std::vector> recipients; - recipients.reserve(recipientsJS.Length()); - for (uint32_t i = 0; i < recipientsJS.Length(); i++) { - auto itemValue = recipientsJS.Get(i); - assertIsUInt8Array(itemValue, "multiEncrypt.itemValue.recipient"); - auto item = toCppBuffer(itemValue, "multiEncrypt.itemValue.recipient"); - recipients.push_back(item); - } - std::vector random_nonce = session::random::random(24); - - std::vector> messages_sv( - messages.begin(), messages.end()); - std::vector> recipients_sv( - recipients.begin(), recipients.end()); - - // Note: this function needs the first 2 args to be vector of sv explicitly - return session::encrypt_for_multiple_simple( - messages_sv, recipients_sv, ed25519SecretKey, domain, random_nonce); - }); - }; - - static Napi::Value multiDecryptEd25519(const Napi::CallbackInfo& info) { - return wrapResult(info, [&] { - assertInfoLength(info, 1); - assertIsObject(info[0]); - auto obj = info[0].As(); - - if (obj.IsEmpty()) - throw std::invalid_argument("multiDecryptEd25519 received empty"); - - assertIsUInt8Array(obj.Get("encoded"), "multiDecryptEd25519.encoded"); - auto encoded = toCppBuffer(obj.Get("encoded"), "multiDecryptEd25519.encoded"); - - assertIsUInt8Array( - obj.Get("userEd25519SecretKey"), "multiDecryptEd25519.userEd25519SecretKey"); - auto ed25519_secret_key = toCppBuffer( - obj.Get("userEd25519SecretKey"), "multiDecryptEd25519.userEd25519SecretKey"); - - assertIsUInt8Array( - obj.Get("senderEd25519Pubkey"), "multiDecryptEd25519.senderEd25519Pubkey"); - auto sender_ed25519_pubkey = toCppBuffer( - obj.Get("senderEd25519Pubkey"), "multiDecryptEd25519.senderEd25519Pubkey"); - - assertIsString(obj.Get("domain")); - auto domain = toCppString(obj.Get("domain"), "multiDecryptEd25519.domain"); - - return session::decrypt_for_multiple_simple_ed25519( - encoded, ed25519_secret_key, sender_ed25519_pubkey, domain); - }); - }; - - static Napi::Value attachmentEncrypt(const Napi::CallbackInfo& info) { - return wrapResult(info, [&] { - assertInfoLength(info, 1); - assertIsObject(info[0]); - auto obj = info[0].As(); - - if (obj.IsEmpty()) - throw std::invalid_argument("attachmentEncrypt received empty"); - - assertIsUInt8Array(obj.Get("seed"), "attachmentEncrypt.seed"); - auto seed = toCppBuffer(obj.Get("seed"), "attachmentEncrypt.seed"); - - assertIsUInt8Array(obj.Get("data"), "attachmentEncrypt.data"); - auto data = toCppBuffer(obj.Get("data"), "attachmentEncrypt.data"); - - assertIsString(obj.Get("domain")); - auto domain = toCppString(obj.Get("domain"), "attachmentEncrypt.domain"); - - assertIsBoolean(obj.Get("allowLarge")); - - auto allow_large = toCppBoolean(obj.Get("allowLarge"), "attachmentEncrypt.allowLarge"); - - if (domain != "attachment" && domain != "profilePic") { - throw std::invalid_argument( - "attachmentEncrypt.domain must be either 'attachment' or 'profilePic'"); - } - - session::attachment::Domain attachment_domain = - domain == "attachment" ? session::attachment::Domain::ATTACHMENT - : session::attachment::Domain::PROFILE_PIC; - - std::vector seed_bytes( - reinterpret_cast(seed.data()), - reinterpret_cast(seed.data() + seed.size())); - - std::vector data_bytes( - reinterpret_cast(data.data()), - reinterpret_cast(data.data() + data.size())); - auto encrypted = session::attachment::encrypt( - seed_bytes, data_bytes, attachment_domain, allow_large); - - auto ret = Napi::Object::New(info.Env()); - ret.Set("encryptedData", toJs(info.Env(), encrypted.first)); - ret.Set("encryptionKey", toJs(info.Env(), encrypted.second)); - - return ret; - }); - }; - - static Napi::Value attachmentDecrypt(const Napi::CallbackInfo& info) { - return wrapResult(info, [&] { - assertInfoLength(info, 1); - assertIsObject(info[0]); - auto obj = info[0].As(); - - if (obj.IsEmpty()) - throw std::invalid_argument("attachmentDecrypt received empty"); - - assertIsUInt8Array(obj.Get("encryptedData"), "attachmentDecrypt.encryptedData"); - auto encrypted_data = - toCppBuffer(obj.Get("encryptedData"), "attachmentDecrypt.encryptedData"); - - assertIsUInt8Array(obj.Get("decryptionKey"), "attachmentDecrypt.decryptionKey"); - auto decryption_key = - toCppBuffer(obj.Get("decryptionKey"), "attachmentDecrypt.decryptionKey"); - - std::vector encrypted_data_bytes( - reinterpret_cast(encrypted_data.data()), - reinterpret_cast( - encrypted_data.data() + encrypted_data.size())); - - std::vector decryption_key_bytes( - reinterpret_cast(decryption_key.data()), - reinterpret_cast( - decryption_key.data() + decryption_key.size())); - - if (decryption_key_bytes.size() != session::attachment::ENCRYPT_KEY_SIZE) { - throw std::invalid_argument("Key size mismatch"); - } - - std::span decryption_key_span( - decryption_key_bytes.data(), session::attachment::ENCRYPT_KEY_SIZE); - - auto decrypted = - session::attachment::decrypt(encrypted_data_bytes, decryption_key_span); - - auto ret = Napi::Object::New(info.Env()); - ret.Set("decryptedData", toJs(info.Env(), decrypted)); - - return ret; - }); - }; -}; - -}; // namespace session::nodeapi diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp new file mode 100644 index 0000000..4f7a62f --- /dev/null +++ b/include/pro/pro.hpp @@ -0,0 +1,146 @@ +#pragma once + +#include +#include + +#include + +#include "../../node_modules/node-addon-api/napi.h" +#include "../meta/meta_base_wrapper.hpp" +#include "../utilities.hpp" +#include "meta/meta_base_wrapper.hpp" +#include "session/pro_backend.hpp" +#include "session/session_protocol.hpp" + +namespace session::nodeapi { + +class ProWrapper : public Napi::ObjectWrap { + + public: + ProWrapper(const Napi::CallbackInfo& info) : Napi::ObjectWrap{info} { + throw std::invalid_argument("ProWrapper is static and doesn't need to be constructed"); + } + + static void Init(Napi::Env env, Napi::Object exports) { + MetaBaseWrapper::NoBaseClassInitHelper( + env, + exports, + "ProWrapperNode", + { + // Pro features + StaticMethod<&ProWrapper::proFeaturesForMessage>( + "proFeaturesForMessage", + static_cast( + napi_writable | napi_configurable)), + StaticMethod<&ProWrapper::proProofRequestBody>( + "proProofRequestBody", + static_cast( + napi_writable | napi_configurable)), + }); + } + + private: + static Napi::Value proFeaturesForMessage(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + // we expect two arguments that match: + // first: { + // "utf16": string, + // "proFeatures": Array, + // } + + assertInfoLength(info, 1); + assertIsObject(info[0]); + auto env = info.Env(); + + auto first = info[0].As(); + + if (first.IsEmpty()) + throw std::invalid_argument("proFeaturesForMessage first received empty"); + + assertIsArray(first.Get("proFeatures"), "proFeaturesForMessage.proFeatures"); + auto proFeaturesJS = first.Get("proFeatures").As(); + std::vector proFeatures; + proFeatures.reserve(proFeaturesJS.Length()); + for (uint32_t i = 0; i < proFeaturesJS.Length(); i++) { + auto itemValue = proFeaturesJS.Get(i); + assertIsString(itemValue, "proFeaturesForMessage.proFeatures.itemValue"); + std::string item = + toCppString(itemValue, "proFeaturesForMessage.proFeatures.itemValue"); + proFeatures.push_back(item); + } + + SESSION_PROTOCOL_PRO_EXTRA_FEATURES flags = 0; + for (std::string& feature : proFeatures) { + // Note: 10K_CHARACTER_LIMIT cannot be requested by the caller + if (feature == "PRO_BADGE") { + flags |= SESSION_PROTOCOL_PRO_EXTRA_FEATURES_PRO_BADGE; + } else if (feature == "ANIMATED_AVATAR") { + flags |= SESSION_PROTOCOL_PRO_EXTRA_FEATURES_ANIMATED_AVATAR; + } + } + assertIsString(first.Get("utf16"), "proFeaturesForMessage.utf16"); + std::u16string utf16 = first.Get("utf16").As().Utf16Value(); + auto pro_features_msg = + session::pro_features_for_utf16((utf16.data()), utf16.length(), flags); + + auto obj = Napi::Object::New(env); + + obj["success"] = toJs(env, pro_features_msg.success); + obj["error"] = + pro_features_msg.error.size() ? toJs(env, pro_features_msg.error) : env.Null(); + obj["codepointCount"] = toJs(env, pro_features_msg.codepoint_count); + obj["proFeatures"] = proFeaturesToJs(env, pro_features_msg.features); + + return obj; + }); + }; + + static Napi::Value proProofRequestBody(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + // we expect arguments that match: + // first: { + // "requestVersion": string, + // "masterPrivkey": Uint8Array, + // "rotatingPrivkey": Uint8Array, + // "unixTsMs": number, + // } + + assertInfoLength(info, 1); + assertIsObject(info[0]); + auto env = info.Env(); + + auto first = info[0].As(); + + if (first.IsEmpty()) + throw std::invalid_argument("proProofRequestBody first received empty"); + + assertIsNumber(first.Get("requestVersion"), "proProofRequestBody.requestVersion"); + assertIsNumber(first.Get("unixTsMs"), "proProofRequestBody.unixTsMs"); + auto requestVersion = first.Get("requestVersion").As(); + auto unix_ts_ms = toCppSysMs(first.Get("unixTsMs"), "proProofRequestBody.unixTsMs"); + + assertIsUInt8Array(first.Get("masterPrivkey"), "proProofRequestBody.masterPrivkey"); + assertIsUInt8Array(first.Get("rotatingPrivkey"), "proProofRequestBody.rotatingPrivkey"); + + auto master_privkey_js = first.Get("masterPrivkey"); + auto rotating_privkey_js = first.Get("rotatingPrivkey"); + auto master_privkey = + toCppBuffer(master_privkey_js, "proProofRequestBody.masterPrivkey"); + auto rotating_privkey = + toCppBuffer(rotating_privkey_js, "proProofRequestBody.rotatingPrivkey"); + + assert_length(master_privkey, 64, "master_privkey"); + assert_length(rotating_privkey, 64, "rotating_prevkey"); + + auto json = pro_backend::GetProProofRequest::build_to_json( + static_cast(requestVersion.Int32Value()), + master_privkey, + rotating_privkey, + unix_ts_ms); + + return json; + }); + }; +}; + +}; // namespace session::nodeapi diff --git a/include/pro/types.hpp b/include/pro/types.hpp new file mode 100644 index 0000000..a8ce087 --- /dev/null +++ b/include/pro/types.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include + +#include "session/config/pro.hpp" +#include "session/session_protocol.hpp" +#include "utilities.hpp" + +namespace session::nodeapi { + +template <> +struct toJs_impl { + Napi::Object operator()(const Napi::Env& env, const session::ProProof pro_proof) { + auto obj = Napi::Object::New(env); + + obj["version"] = toJs(env, pro_proof.version); + obj["genIndexHashB64"] = toJs(env, oxenc::to_base64(pro_proof.gen_index_hash)); + obj["rotatingPubkeyHex"] = toJs(env, oxenc::to_hex(pro_proof.rotating_pubkey)); + obj["expiryMs"] = toJs(env, pro_proof.expiry_unix_ts.time_since_epoch().count()); + + return obj; + } +}; + +template <> +struct toJs_impl { + Napi::Value operator()(const Napi::Env& env, const session::config::ProConfig pro_config) { + auto obj = Napi::Object::New(env); + + obj["rotatingPrivKeyHex"] = toJs(env, oxenc::to_hex(pro_config.rotating_privkey)); + obj["proProof"] = toJs(env, pro_config.proof); + + return obj; + } +}; + +template <> +struct toJs_impl { + Napi::Object operator()(const Napi::Env& env, const session::Envelope envelope) { + auto obj = Napi::Object::New(env); + + obj["timestampMs"] = toJs(env, envelope.timestamp.count()); + obj["source"] = envelope.source.size() ? toJs(env, envelope.source) : env.Null(); + obj["proSigHex"] = + envelope.pro_sig.size() ? toJs(env, oxenc::to_hex(envelope.pro_sig)) : env.Null(); + + return obj; + } +}; + +template <> +struct toJs_impl { + Napi::Object operator()(const Napi::Env& env, const session::DecodedEnvelope decoded_envelope) { + auto obj = Napi::Object::New(env); + + obj.Set("envelope", toJs(env, decoded_envelope.envelope)); + obj.Set("contentPlaintextUnpadded", toJs(env, decoded_envelope.content_plaintext)); + obj.Set("sessionId", + toJs(env, "05" + oxenc::to_hex(decoded_envelope.sender_x25519_pubkey))); + obj.Set("decodedPro", decoded_envelope.pro ? toJs(env, decoded_envelope.pro) : env.Null()); + + return obj; + } +}; + +template <> +struct toJs_impl { + Napi::Object operator()(const Napi::Env& env, const session::DecodedPro decoded_pro) { + auto obj = Napi::Object::New(env); + + obj["proStatus"] = + toJs(env, + decoded_pro.status == ProStatus::InvalidProBackendSig ? "InvalidProBackendSig" + : decoded_pro.status == ProStatus::InvalidUserSig ? "InvalidUserSig" + : decoded_pro.status == ProStatus::Valid ? "Valid" + : "Expired"); + obj["proProof"] = toJs(env, decoded_pro.proof); + obj["proFeatures"] = proFeaturesToJs(env, decoded_pro.features); + + return obj; + } +}; + +}; // namespace session::nodeapi \ No newline at end of file diff --git a/include/profile_pic.hpp b/include/profile_pic.hpp index e1b2c67..9110036 100644 --- a/include/profile_pic.hpp +++ b/include/profile_pic.hpp @@ -5,7 +5,6 @@ namespace session::nodeapi { - // Constructs a profile_pic from a Napi::Value which must be either Null or an Object; if an // Object then it *must* contain "url" (string or null) and "key" (uint8array of size 32 or // null) keys; if either is empty or Null then you get back an empty (i.e. clearing) diff --git a/include/user_config.hpp b/include/user_config.hpp index 1ac3abb..ba4ca5d 100644 --- a/include/user_config.hpp +++ b/include/user_config.hpp @@ -34,6 +34,9 @@ class UserConfigWrapper : public ConfigBaseImpl, public Napi::ObjectWrap #include +#include "oxen/log/catlogger.hpp" +#include "oxenc/hex.h" #include "session/config/namespaces.hpp" #include "session/config/profile_pic.hpp" +#include "session/session_protocol.h" +#include "session/types.h" #include "session/types.hpp" -#include "utilities.hpp" namespace session::nodeapi { using namespace std::literals; +inline auto cat = oxen::log::Cat("nodeapi"); + static void checkOrThrow(bool condition, const char* msg) { if (!condition) throw std::invalid_argument{msg}; @@ -28,14 +33,14 @@ void assertInfoLength(const Napi::CallbackInfo& info, const int expected); void assertInfoMinLength(const Napi::CallbackInfo& info, const int minLength); -void assertIsStringOrNull(const Napi::Value& value); +void assertIsStringOrNull(const Napi::Value& value, const std::string& identifier = ""); void assertIsNumber(const Napi::Value& value, const std::string& identifier); void assertIsArray(const Napi::Value& value, const std::string& identifier); void assertIsObject(const Napi::Value& value); void assertIsUInt8ArrayOrNull(const Napi::Value& value); void assertIsUInt8Array(const Napi::Value& value, const std::string& identifier); -void assertIsString(const Napi::Value& value); -void assertIsBoolean(const Napi::Value& value); +void assertIsString(const Napi::Value& value, const std::string& identifier = ""); +void assertIsBoolean(const Napi::Value& value, const std::string& identifier = ""); // Checks for and returns exactly N string arguments. If N == 1 this return just a string; if > 1 // this returns an std::array of strings of size N. @@ -45,7 +50,7 @@ auto getStringArgs(const Napi::CallbackInfo& info) { std::array args; for (int i = 0; i < args.size(); i++) { auto arg = info[i]; - assertIsString(arg); + assertIsString(arg, "getStringArgs"); args[i] = arg.As().Utf8Value(); } if constexpr (N == 1) @@ -55,6 +60,7 @@ auto getStringArgs(const Napi::CallbackInfo& info) { } std::string toCppString(Napi::Value x, const std::string& identifier); +std::span toCppBufferView(Napi::Value x, const std::string& identifier); std::vector toCppBuffer(Napi::Value x, const std::string& identifier); int64_t toCppInteger(Napi::Value x, const std::string& identifier, bool allowUndefined = false); @@ -64,6 +70,9 @@ std::optional maybeNonemptySysSeconds( Napi::Value x, const std::string& identifier); std::chrono::sys_seconds toCppSysSeconds(Napi::Value x, const std::string& identifier); +std::chrono::sys_time toCppSysMs( + Napi::Value x, const std::string& identifier); +std::chrono::milliseconds toCppMs(Napi::Value x, const std::string& identifier); bool toCppBoolean(Napi::Value x, const std::string& identifier); @@ -114,6 +123,11 @@ struct toJs_impl { } }; +template <> +struct toJs_impl { + auto operator()(const Napi::Env& env, size_t b) const { return Napi::Number::New(env, (b)); } +}; + template struct toJs_impl>> { auto operator()(const Napi::Env& env, T n) const { return Napi::Number::New(env, n); } @@ -126,6 +140,13 @@ struct toJs_impl> } }; +template <> +struct toJs_impl { + auto operator()(const Napi::Env& env, string8 s) const { + return Napi::String::New(env, s.data, s.size); + } +}; + template struct toJs_impl< T, @@ -150,10 +171,7 @@ template <> struct toJs_impl> { auto operator()(const Napi::Env& env, std::vector b) const { return Napi::Buffer::Copy( - env, - reinterpret_cast(b.data()), - b.size() - ); + env, reinterpret_cast(b.data()), b.size()); } }; @@ -285,7 +303,7 @@ auto wrapResult(const Napi::Env& env, Call&& call) { if constexpr (std::is_base_of_v) return res; else - return toJs(env, std::move(res)); + return toJs(env, std::move(res)); } } catch (const std::exception& e) { throw Napi::Error::New(env, e.what()); @@ -353,4 +371,47 @@ Napi::Object decrypt_result_to_JS( confirm_pushed_entry_t confirm_pushed_entry_from_JS(const Napi::Env& env, const Napi::Object& obj); +Napi::Object proFeaturesToJs(const Napi::Env& env, const SESSION_PROTOCOL_PRO_FEATURES bitset); + +std::span from_hex_to_span(std::string_view x); + +template +std::array spanToArray(std::span span); + +template +std::array from_hex_to_array(std::string x) { + std::string as_hex = oxenc::from_hex(x); + if (as_hex.size() != N) { + throw std::invalid_argument(std::format( + "from_hex_to_array: Decoded hex size mismatch: expected {}, got {}", + N, + as_hex.size())); + } + + std::array result; + std::memcpy(result.data(), as_hex.data(), N); + return result; +} + +std::vector from_hex_to_vector(std::string_view x); + +std::span from_base64_to_span(std::string_view x); +std::vector from_base64_to_vector(std::string_view x); + +// Concept to match containers with a size() method +template +concept HasSize = requires(T t) { + { + t.size() + } -> std::convertible_to; +}; + +template +void assert_length(const T& x, size_t n, std::string_view base_identifier) { + if (x.size() != n) { + throw std::invalid_argument(std::format( + "assert_length: expected {}, got {} for {}", n, x.size(), base_identifier)); + } +} + } // namespace session::nodeapi diff --git a/libsession-util b/libsession-util index 1fdd145..5fd7882 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 1fdd145076cb932a78fdde6e37753bacc2848fff +Subproject commit 5fd78825b79dbc9dbabd06cd64ae37e845d95904 diff --git a/package.json b/package.json index 7a948f5..934b00c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "scripts": { "update_version": "sh update_version.sh", "clean": "rimraf .cache build", - "lint:cpp": "cppcheck --std=c++20 -j8 --quiet src libsession-util/src", + "lint:cpp": "cppcheck --std=c++20 -j8 src", + "lint": "find src include -name '*.cpp' -o -name '*.hpp' | xargs clang-format-19 -i", "install": "cmake-js build --runtime=electron --runtime-version=34.2.0 --CDSUBMODULE_CHECK=OFF --CDLOCAL_MIRROR=https://oxen.rocks/deps --CDENABLE_ONIONREQ=OFF --CDWITH_TESTS=OFF", "prepare_release": "sh prepare_release.sh", "dedup": "npx --yes yarn-deduplicate yarn.lock" diff --git a/src/addon.cpp b/src/addon.cpp index f9f4806..15795ab 100644 --- a/src/addon.cpp +++ b/src/addon.cpp @@ -6,8 +6,9 @@ #include "constants.hpp" #include "contacts_config.hpp" #include "convo_info_volatile_config.hpp" +#include "encrypt_decrypt/encrypt_decrypt.hpp" #include "groups/meta_group_wrapper.hpp" -#include "multi_encrypt/multi_encrypt.hpp" +#include "pro/pro.hpp" #include "user_config.hpp" #include "user_groups_config.hpp" @@ -32,7 +33,7 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports) { .As() .Get("log") .As(); - Napi::String jsStr = Napi::String::New(env, "libsession-util: " + *msg); + Napi::String jsStr = Napi::String::New(env, "libsession: " + *msg); consoleLog.Call({jsStr}); delete msg; }); @@ -52,6 +53,7 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports) { // Fully static wrappers init session::nodeapi::MultiEncryptWrapper::Init(env, exports); + session::nodeapi::ProWrapper::Init(env, exports); session::nodeapi::BlindingWrapper::Init(env, exports); return exports; diff --git a/src/base_config.cpp b/src/base_config.cpp index 2c66de1..b38b3fc 100644 --- a/src/base_config.cpp +++ b/src/base_config.cpp @@ -1,7 +1,6 @@ #include "base_config.hpp" #include "session/config/base.hpp" -#include "session/config/encrypt.hpp" namespace session::nodeapi { diff --git a/src/constants.cpp b/src/constants.cpp index a47ca65..49e3a10 100644 --- a/src/constants.cpp +++ b/src/constants.cpp @@ -1,9 +1,12 @@ #include "constants.hpp" +#include "js_native_api_types.h" #include "session/config/contacts.hpp" #include "session/config/groups/info.hpp" #include "session/config/user_groups.hpp" +#include "session/pro_backend.h" #include "session/version.h" +#include "utilities.hpp" #include "version.h" namespace session::nodeapi { @@ -13,6 +16,150 @@ ConstantsWrapper::ConstantsWrapper(const Napi::CallbackInfo& info) : Napi::Object ConstantsWrapper::Init(Napi::Env env, Napi::Object exports) { const char* class_name = "CONSTANTS"; + auto pro_urls = Napi::Object::New(env); + pro_urls["roadmap"] = toJs(env, SESSION_PRO_URLS.roadmap); + pro_urls["privacy_policy"] = toJs(env, SESSION_PRO_URLS.privacy_policy); + pro_urls["terms_of_service"] = toJs(env, SESSION_PRO_URLS.terms_of_service); + pro_urls["pro_access_not_found"] = toJs(env, SESSION_PRO_URLS.pro_access_not_found); + pro_urls["support_url"] = toJs(env, SESSION_PRO_URLS.support_url); + + auto pro_provider_nil = Napi::Object::New(env); + pro_provider_nil["device"] = toJs( + env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL] + .device); + pro_provider_nil["store"] = toJs( + env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL] + .store); + pro_provider_nil["platform"] = toJs( + env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL] + .platform); + pro_provider_nil["platform_account"] = toJs( + env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL] + .platform_account); + pro_provider_nil["refund_url"] = toJs( + env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL] + .refund_url); + pro_provider_nil["refund_after_platform_deadline_url"] = toJs( + env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL] + .refund_after_platform_deadline_url); + pro_provider_nil["update_subscription_url"] = toJs( + env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL] + .update_subscription_url); + pro_provider_nil["cancel_subscription_url"] = toJs( + env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL] + .cancel_subscription_url); + pro_provider_nil["store_other"] = toJs( + env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL] + .store); + + auto pro_provider_google = Napi::Object::New(env); + pro_provider_google["device"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE] + .device); + pro_provider_google["store"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE] + .store); + pro_provider_google["platform"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE] + .platform); + pro_provider_google["platform_account"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE] + .platform_account); + pro_provider_google["refund_url"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE] + .refund_url); + pro_provider_google["refund_after_platform_deadline_url"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE] + .refund_after_platform_deadline_url); + pro_provider_google["update_subscription_url"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE] + .update_subscription_url); + pro_provider_google["cancel_subscription_url"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE] + .cancel_subscription_url); + pro_provider_google["store_other"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE] + .store); + + auto pro_provider_ios = Napi::Object::New(env); + pro_provider_ios["device"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE] + .device); + pro_provider_ios["store"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE] + .store); + pro_provider_ios["platform"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE] + .platform); + pro_provider_ios["platform_account"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE] + .platform_account); + pro_provider_ios["refund_url"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE] + .refund_url); + pro_provider_ios["refund_after_platform_deadline_url"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE] + .refund_after_platform_deadline_url); + pro_provider_ios["update_subscription_url"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE] + .update_subscription_url); + pro_provider_ios["cancel_subscription_url"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE] + .cancel_subscription_url); + pro_provider_ios["store_other"] = + toJs(env, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + [SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE] + .store); + + auto pro_providers = Napi::Object::New(env); + pro_providers["Nil"] = toJs(env, pro_provider_nil); + pro_providers["Google"] = toJs(env, pro_provider_google); + pro_providers["iOS"] = toJs(env, pro_provider_ios); + // construct javascript constants object Napi::Function cls = DefineClass( env, @@ -37,6 +184,8 @@ Napi::Object ConstantsWrapper::Init(Napi::Env env, Napi::Object exports) { "COMMUNITY_FULL_URL_MAX_LENGTH", Napi::Number::New(env, session::config::community::FULL_URL_MAX_LENGTH), napi_enumerable), + ObjectWrap::StaticValue("LIBSESSION_PRO_URLS", pro_urls, napi_enumerable), + ObjectWrap::StaticValue("LIBSESSION_PRO_PROVIDERS", pro_providers, napi_enumerable), ObjectWrap::StaticValue( "LIBSESSION_UTIL_VERSION", Napi::String::New(env, LIBSESSION_UTIL_VERSION_FULL), diff --git a/src/encrypt_decrypt/encrypt_decrypt.cpp b/src/encrypt_decrypt/encrypt_decrypt.cpp new file mode 100644 index 0000000..753ee6e --- /dev/null +++ b/src/encrypt_decrypt/encrypt_decrypt.cpp @@ -0,0 +1,863 @@ +#include "encrypt_decrypt/encrypt_decrypt.hpp" + +#include +#include +#include + +#include +#include + +#include "pro/types.hpp" +#include "session/attachments.hpp" +#include "session/multi_encrypt.hpp" +#include "session/random.hpp" +#include "utilities.hpp" + +namespace session::nodeapi { + +namespace log = oxen::log; + +std::vector extractPlaintext(const Napi::Object& obj, const std::string identifier) { + + assertIsUInt8Array(obj.Get("plaintext"), identifier); + auto plaintext = toCppBuffer(obj.Get("plaintext"), identifier); + + return plaintext; +} + +std::chrono::milliseconds extractSentTimestampMs( + const Napi::Object& obj, const std::string identifier) { + assertIsNumber(obj.Get("sentTimestampMs"), identifier); + auto sentTimestampMs = toCppMs(obj.Get("sentTimestampMs"), identifier); + + return sentTimestampMs; +} + +std::vector extractSenderEd25519SeedAsVector( + const Napi::Object& obj, const std::string identifier) { + + assertIsUInt8Array( + obj.Get("senderEd25519Seed"), "extractSenderEd25519SeedAsVector.senderEd25519Seed"); + + auto senderEd25519Seed = toCppBuffer(obj.Get("senderEd25519Seed"), identifier); + assert_length(senderEd25519Seed, 32, identifier); + + return senderEd25519Seed; +} + +session::array_uc33 extractRecipientPubkeyAsArray( + const Napi::Object& obj, const std::string identifier) { + assertIsString(obj.Get("recipientPubkey"), identifier); + auto recipientPubkeyHex = toCppString(obj.Get("recipientPubkey"), identifier); + assert_length(recipientPubkeyHex, 66, identifier); + + return from_hex_to_array<33>(recipientPubkeyHex); +} + +session::array_uc32 extractCommunityPubkeyAsArray( + const Napi::Object& obj, const std::string identifier) { + assertIsString(obj.Get("communityPubkey"), identifier); + auto communityPubkeyHex = toCppString(obj.Get("communityPubkey"), identifier); + assert_length(communityPubkeyHex, 64, identifier); + + return from_hex_to_array<32>(communityPubkeyHex); +} + +session::array_uc33 extractGroupEd25519PubkeyAsArray( + const Napi::Object& obj, const std::string identifier) { + assertIsString(obj.Get("groupEd25519Pubkey"), identifier); + std::string groupEd25519PubkeyHex = toCppString(obj.Get("groupEd25519Pubkey"), identifier); + + assert_length(groupEd25519PubkeyHex, 66, identifier); + auto arr = from_hex_to_array<33>(groupEd25519PubkeyHex); + + return arr; +} + +cleared_uc32 extractGroupEncKeyAsArray(const Napi::Object& obj, const std::string identifier) { + assertIsString(obj.Get("groupEncKey"), identifier); + + auto groupEncKeyHex = toCppString(obj.Get("groupEncKey"), identifier); + assert_length(groupEncKeyHex, 64, identifier); + + auto arr = from_hex_to_array<32>(groupEncKeyHex); + cleared_uc32 result; + + std::copy(arr.begin(), arr.end(), result.begin()); + + return result; +} + +std::optional> extractProRotatingEd25519PrivKeyAsVector( + const Napi::Object& obj, const std::string identifier) { + assertIsStringOrNull(obj.Get("proRotatingEd25519PrivKey"), identifier); + auto proRotatingEd25519PrivKeyHex = + maybeNonemptyString(obj.Get("proRotatingEd25519PrivKey"), identifier); + + if (proRotatingEd25519PrivKeyHex.has_value() && proRotatingEd25519PrivKeyHex.value().size()) { + assert_length(*proRotatingEd25519PrivKeyHex, 64, identifier); + + auto ret = from_hex_to_vector(*proRotatingEd25519PrivKeyHex); + + return ret; + } + + return std::nullopt; +} + +std::vector extractContentOrEnvelope( + const Napi::Object& obj, const std::string identifier) { + assertIsUInt8Array(obj.Get("contentOrEnvelope"), identifier); + std::vector contentOrEnvelope = + toCppBuffer(obj.Get("contentOrEnvelope"), identifier); + + return contentOrEnvelope; +} + +uint32_t extractServerId(const Napi::Object& obj, const std::string identifier) { + assertIsNumber(obj.Get("serverId"), identifier); + auto serverId = toCppInteger(obj.Get("serverId"), identifier); + + return serverId; +} + +std::string extractMessageHash(const Napi::Object& obj, const std::string identifier) { + assertIsString(obj.Get("messageHash"), identifier); + auto messageHash = toCppString(obj.Get("messageHash"), identifier); + + return messageHash; +} + +std::vector extractEnvelopePayload( + const Napi::Object& obj, const std::string identifier) { + assertIsUInt8Array(obj.Get("envelopePayload"), identifier); + auto envelopePayload = toCppBuffer(obj.Get("envelopePayload"), identifier); + + return envelopePayload; +} + +std::chrono::sys_time extractNowSysMs( + const Napi::Object& obj, const std::string identifier) { + assertIsNumber(obj.Get("nowMs"), identifier); + auto nowMs = toCppSysMs(obj.Get("nowMs"), identifier); + + return nowMs; +} + +session::array_uc32 extractProBackendPubkeyHex( + const Napi::Object& obj, const std::string identifier) { + assertIsString(obj.Get("proBackendPubkeyHex"), identifier); + + auto proBackendPubkeyHex = toCppString(obj.Get("proBackendPubkeyHex"), identifier); + assert_length(proBackendPubkeyHex, 64, identifier); + + auto arr = from_hex_to_array<32>(proBackendPubkeyHex); + + return arr; +} + +session::array_uc32 extractEd25519PrivateKeyHex( + const Napi::Object& obj, const std::string identifier) { + assertIsString(obj.Get("ed25519PrivateKeyHex"), identifier); + + auto ed25519PrivateKeyHex = toCppString(obj.Get("ed25519PrivateKeyHex"), identifier); + assert_length(ed25519PrivateKeyHex, 64, identifier); + + auto arr = from_hex_to_array<32>(ed25519PrivateKeyHex); + + return arr; +} + +std::vector extractEd25519GroupPubkeyHex( + const Napi::Object& obj, const std::string& identifier) { + assertIsString(obj.Get("ed25519GroupPubkeyHex"), identifier); + auto ed25519GroupPubkeyHex = toCppString(obj.Get("ed25519GroupPubkeyHex"), identifier); + assert_length(ed25519GroupPubkeyHex, 66, identifier); + + auto arr = from_hex_to_vector(ed25519GroupPubkeyHex); + + return arr; +} + +std::vector> extractGroupEncKeys( + const Napi::Object& obj, const std::string& identifier) { + assertIsArray(obj.Get("groupEncKeys"), identifier); + + auto asArray = obj.Get("groupEncKeys").As(); + std::vector> groupEncKeys; + groupEncKeys.reserve(asArray.Length()); + + for (uint32_t i = 0; i < asArray.Length(); i++) { + auto itemValue = asArray.Get(i); + assertIsUInt8Array(itemValue, "extractGroupEncKeys"); + + auto encKey = itemValue.As(); + + std::vector cppEncKey = + toCppBuffer(encKey, "extractGroupEncKeys.groupEncKey"); + assert_length(cppEncKey, 32, "extractGroupEncKeys.groupEncKey"); + + groupEncKeys.emplace_back(cppEncKey); + } + + return groupEncKeys; +} + +Napi::Value MultiEncryptWrapper::multiEncrypt(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsObject(info[0]); + auto obj = info[0].As(); + + if (obj.IsEmpty()) + throw std::invalid_argument("multiEncrypt received empty"); + + assertIsUInt8Array(obj.Get("ed25519SecretKey"), "multiEncrypt.ed25519SecretKey"); + auto ed25519SecretKey = + toCppBuffer(obj.Get("ed25519SecretKey"), "multiEncrypt.ed25519SecretKey"); + + assertIsString(obj.Get("domain"), "multiEncrypt.domain"); + auto domain = toCppString(obj.Get("domain"), "multiEncrypt.domain"); + + // handle the messages conversion + auto messagesJSValue = obj.Get("messages"); + assertIsArray(messagesJSValue, "multiEncrypt"); + auto messagesJS = messagesJSValue.As(); + std::vector> messages; + messages.reserve(messagesJS.Length()); + for (uint32_t i = 0; i < messagesJS.Length(); i++) { + auto itemValue = messagesJS.Get(i); + assertIsUInt8Array(itemValue, "multiEncrypt.itemValue.message"); + auto item = toCppBuffer(itemValue, "multiEncrypt.itemValue.message"); + messages.push_back(item); + } + + // handle the recipients conversion + auto recipientsJSValue = obj.Get("recipients"); + assertIsArray(recipientsJSValue, "multiEncrypt"); + auto recipientsJS = recipientsJSValue.As(); + std::vector> recipients; + recipients.reserve(recipientsJS.Length()); + for (uint32_t i = 0; i < recipientsJS.Length(); i++) { + auto itemValue = recipientsJS.Get(i); + assertIsUInt8Array(itemValue, "multiEncrypt.itemValue.recipient"); + auto item = toCppBuffer(itemValue, "multiEncrypt.itemValue.recipient"); + recipients.push_back(item); + } + std::vector random_nonce = session::random::random(24); + + std::vector> messages_sv(messages.begin(), messages.end()); + std::vector> recipients_sv( + recipients.begin(), recipients.end()); + + // Note: this function needs the first 2 args to be vector of sv explicitly + return session::encrypt_for_multiple_simple( + messages_sv, recipients_sv, ed25519SecretKey, domain, random_nonce); + }); +}; + +Napi::Value MultiEncryptWrapper::multiDecryptEd25519(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsObject(info[0]); + auto obj = info[0].As(); + + if (obj.IsEmpty()) + throw std::invalid_argument("multiDecryptEd25519 received empty"); + + assertIsUInt8Array(obj.Get("encoded"), "multiDecryptEd25519.encoded"); + auto encoded = toCppBuffer(obj.Get("encoded"), "multiDecryptEd25519.encoded"); + + assertIsUInt8Array( + obj.Get("userEd25519SecretKey"), "multiDecryptEd25519.userEd25519SecretKey"); + auto ed25519_secret_key = toCppBuffer( + obj.Get("userEd25519SecretKey"), "multiDecryptEd25519.userEd25519SecretKey"); + + assertIsUInt8Array( + obj.Get("senderEd25519Pubkey"), "multiDecryptEd25519.senderEd25519Pubkey"); + auto sender_ed25519_pubkey = toCppBuffer( + obj.Get("senderEd25519Pubkey"), "multiDecryptEd25519.senderEd25519Pubkey"); + + assertIsString(obj.Get("domain"), "multiDecryptEd25519.domain"); + auto domain = toCppString(obj.Get("domain"), "multiDecryptEd25519.domain"); + + return session::decrypt_for_multiple_simple_ed25519( + encoded, ed25519_secret_key, sender_ed25519_pubkey, domain); + }); +}; + +/** + * =========================================== + * =========== ATTACHMENTS CALLS ============= + * =========================================== + */ + +Napi::Value MultiEncryptWrapper::attachmentEncrypt(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsObject(info[0]); + auto obj = info[0].As(); + + if (obj.IsEmpty()) + throw std::invalid_argument("attachmentEncrypt received empty"); + + assertIsUInt8Array(obj.Get("seed"), "attachmentEncrypt.seed"); + auto seed = toCppBuffer(obj.Get("seed"), "attachmentEncrypt.seed"); + + assertIsUInt8Array(obj.Get("data"), "attachmentEncrypt.data"); + auto data = toCppBuffer(obj.Get("data"), "attachmentEncrypt.data"); + + assertIsString(obj.Get("domain"), "attachmentEncrypt.domain"); + auto domain = toCppString(obj.Get("domain"), "attachmentEncrypt.domain"); + + assertIsBoolean(obj.Get("allowLarge")); + + auto allow_large = toCppBoolean(obj.Get("allowLarge"), "attachmentEncrypt.allowLarge"); + + if (domain != "attachment" && domain != "profilePic") { + throw std::invalid_argument( + "attachmentEncrypt.domain must be either 'attachment' or 'profilePic'"); + } + + session::attachment::Domain attachment_domain = + domain == "attachment" ? session::attachment::Domain::ATTACHMENT + : session::attachment::Domain::PROFILE_PIC; + + std::vector seed_bytes( + reinterpret_cast(seed.data()), + reinterpret_cast(seed.data() + seed.size())); + + std::vector data_bytes( + reinterpret_cast(data.data()), + reinterpret_cast(data.data() + data.size())); + auto encrypted = session::attachment::encrypt( + seed_bytes, data_bytes, attachment_domain, allow_large); + + auto ret = Napi::Object::New(info.Env()); + ret.Set("encryptedData", toJs(info.Env(), encrypted.first)); + ret.Set("encryptionKey", toJs(info.Env(), encrypted.second)); + + return ret; + }); +}; + +Napi::Value MultiEncryptWrapper::attachmentDecrypt(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + assertIsObject(info[0]); + auto obj = info[0].As(); + + if (obj.IsEmpty()) + throw std::invalid_argument("attachmentDecrypt received empty"); + + assertIsUInt8Array(obj.Get("encryptedData"), "attachmentDecrypt.encryptedData"); + auto encrypted_data = + toCppBuffer(obj.Get("encryptedData"), "attachmentDecrypt.encryptedData"); + + assertIsUInt8Array(obj.Get("decryptionKey"), "attachmentDecrypt.decryptionKey"); + auto decryption_key = + toCppBuffer(obj.Get("decryptionKey"), "attachmentDecrypt.decryptionKey"); + + std::vector encrypted_data_bytes( + reinterpret_cast(encrypted_data.data()), + reinterpret_cast(encrypted_data.data() + encrypted_data.size())); + + std::vector decryption_key_bytes( + reinterpret_cast(decryption_key.data()), + reinterpret_cast(decryption_key.data() + decryption_key.size())); + + if (decryption_key_bytes.size() != session::attachment::ENCRYPT_KEY_SIZE) { + throw std::invalid_argument("Key size mismatch"); + } + + std::span decryption_key_span( + decryption_key_bytes.data(), session::attachment::ENCRYPT_KEY_SIZE); + + auto decrypted = session::attachment::decrypt(encrypted_data_bytes, decryption_key_span); + + auto ret = Napi::Object::New(info.Env()); + ret.Set("decryptedData", toJs(info.Env(), decrypted)); + + return ret; + }); +}; + +/** + * =========================================== + * ============= ENCRYPT CALLS =============== + * =========================================== + */ + +Napi::Value MultiEncryptWrapper::encryptFor1o1(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + // we expect a single argument which is an array of objects with the following + // properties: + // { + // "plaintext": Uint8Array, + // "sentTimestampMs": Number, + // "senderEd25519Seed": Hexstring, + // "recipientPubkey": Hexstring, + // "proRotatingEd25519PrivKey": Hexstring | null, + // } + // + + assertInfoLength(info, 1); + assertIsArray(info[0], "encryptFor1o1 info[0]"); + + auto array = info[0].As(); + + if (array.IsEmpty()) + throw std::invalid_argument("encryptFor1o1 received empty"); + + std::vector> ready_to_send(array.Length()); + for (uint32_t i = 0; i < array.Length(); i++) { + auto itemValue = array.Get(i); + if (!itemValue.IsObject()) { + throw std::invalid_argument("encryptFor1o1 itemValue is not an object"); + } + auto obj = itemValue.As(); + + ready_to_send[i] = session::encode_for_1o1( + extractPlaintext(obj, "encryptFor1o1.obj.plaintext"), + extractSenderEd25519SeedAsVector(obj, "encryptFor1o1.obj.senderEd25519Seed"), + extractSentTimestampMs(obj, "encryptFor1o1.obj.sentTimestampMs"), + extractRecipientPubkeyAsArray(obj, "encryptFor1o1.obj.recipientPubkey"), + extractProRotatingEd25519PrivKeyAsVector( + obj, "encryptFor1o1.obj.proRotatingEd25519PrivKey")); + } + + auto ret = Napi::Object::New(info.Env()); + ret.Set("encryptedData", toJs(info.Env(), ready_to_send)); + + return ret; + }); +}; + +Napi::Value MultiEncryptWrapper::encryptForCommunityInbox(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + // we expect a single argument which is an array of objects with the following + // properties: + // { + // "plaintext": Uint8Array, + // "senderEd25519Seed": Hexstring, + // "sentTimestampMs": Number, + // "recipientPubkey": Hexstring, + // "communityPubkey": Hexstring, + // "proRotatingEd25519PrivKey": Hexstring | null, + // } + // + + assertInfoLength(info, 1); + assertIsArray(info[0], "encryptForCommunityInbox info[0]"); + + auto array = info[0].As(); + + if (array.IsEmpty()) + throw std::invalid_argument("encryptForCommunityInbox received empty"); + + std::vector> ready_to_send(array.Length()); + for (uint32_t i = 0; i < array.Length(); i++) { + auto itemValue = array.Get(i); + if (!itemValue.IsObject()) { + throw std::invalid_argument("encryptForCommunityInbox itemValue is not an object"); + } + auto obj = itemValue.As(); + + ready_to_send[i] = session::encode_for_community_inbox( + extractPlaintext(obj, "encryptForCommunityInbox.obj.plaintext"), + extractSenderEd25519SeedAsVector( + obj, "encryptForCommunityInbox.obj.senderEd25519Seed"), + extractSentTimestampMs(obj, "encryptForCommunityInbox.obj.sentTimestampMs"), + extractRecipientPubkeyAsArray( + obj, "encryptForCommunityInbox.obj.recipientPubkey"), + extractCommunityPubkeyAsArray( + obj, "encryptForCommunityInbox.obj.communityPubkey"), + extractProRotatingEd25519PrivKeyAsVector( + obj, "encryptForCommunityInbox.obj.proRotatingEd25519PrivKey")); + } + + auto ret = Napi::Object::New(info.Env()); + ret.Set("encryptedData", toJs(info.Env(), ready_to_send)); + + return ret; + }); +}; + +Napi::Value MultiEncryptWrapper::encryptForCommunity(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + // we expect a single argument which is an array of objects with the following + // properties: + // { + // "plaintext": Uint8Array, + // "proRotatingEd25519PrivKey": Hexstring | null, + // } + // + + assertInfoLength(info, 1); + assertIsArray(info[0], "encryptForCommunity info[0]"); + + auto array = info[0].As(); + + if (array.IsEmpty()) + throw std::invalid_argument("encryptForCommunity received empty"); + + std::vector> ready_to_send(array.Length()); + for (uint32_t i = 0; i < array.Length(); i++) { + auto itemValue = array.Get(i); + if (!itemValue.IsObject()) { + throw std::invalid_argument("encryptForCommunity itemValue is not an object"); + } + auto obj = itemValue.As(); + + ready_to_send[i] = session::encode_for_community( + extractPlaintext(obj, "encryptForCommunity.obj.plaintext"), + extractProRotatingEd25519PrivKeyAsVector( + obj, "encryptForCommunity.obj.proRotatingEd25519PrivKey")); + } + + auto ret = Napi::Object::New(info.Env()); + ret.Set("encryptedData", toJs(info.Env(), ready_to_send)); + + return ret; + }); +}; + +Napi::Value MultiEncryptWrapper::encryptForGroup(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + // we expect a single argument which is an array of objects with the following + // properties: + // { + // "plaintext": Uint8Array, + // "senderEd25519Seed": Uint8Array, 32 bytes + // "sentTimestampMs": Number, + // "groupEd25519Pubkey": Hexstring, + // "groupEncKey": Hexstring, + // "proRotatingEd25519PrivKey": Hexstring | null, + // } + // + + assertInfoLength(info, 1); + assertIsArray(info[0], "encryptForGroup info[0]"); + + auto array = info[0].As(); + + if (array.IsEmpty()) + throw std::invalid_argument("encryptForGroup received empty"); + + std::vector> ready_to_send(array.Length()); + for (uint32_t i = 0; i < array.Length(); i++) { + auto itemValue = array.Get(i); + if (!itemValue.IsObject()) { + throw std::invalid_argument("encryptForGroup itemValue is not an object"); + } + auto obj = itemValue.As(); + + auto plaintext = extractPlaintext(obj, "encryptForGroup.obj.plaintext"); + auto senderEd25519Seed = + extractSenderEd25519SeedAsVector(obj, "encryptForGroup.obj.senderEd25519Seed"); + + auto sentTimestampMs = + extractSentTimestampMs(obj, "encryptForGroup.obj.sentTimestampMs"); + + auto groupEd25519Pubkey = + extractGroupEd25519PubkeyAsArray(obj, "encryptForGroup.obj.recipientPubkey"); + + auto groupEncKey = extractGroupEncKeyAsArray(obj, "encryptForGroup.obj.groupEncKey"); + auto proRotatingEd25519PrivKey = extractProRotatingEd25519PrivKeyAsVector( + obj, "encryptForGroup.obj.proRotatingEd25519PrivKey"); + + ready_to_send[i] = session::encode_for_group( + plaintext, + senderEd25519Seed, + sentTimestampMs, + groupEd25519Pubkey, + groupEncKey, + proRotatingEd25519PrivKey); + } + + auto ret = Napi::Object::New(info.Env()); + ret.Set("encryptedData", toJs(info.Env(), ready_to_send)); + + return ret; + }); +}; + +/** + * =========================================== + * ============= DECRYPT CALLS =============== + * =========================================== + */ + +Napi::Value MultiEncryptWrapper::decryptForCommunity(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + // we expect two arguments that match: + // first: [{ + // "contentOrEnvelope": Uint8Array, + // "serverId": number, + // }], + // second: { + // "nowMs": number, + // "proBackendPubkeyHex": Hexstring, + // } + // + + assertInfoLength(info, 2); + assertIsArray(info[0], "decryptForCommunity info[0]"); + assertIsObject(info[1]); + + auto first = info[0].As(); + + if (first.IsEmpty()) + throw std::invalid_argument("decryptForCommunity first received empty"); + + auto second = info[1].As(); + + if (second.IsEmpty()) + throw std::invalid_argument("decryptForCommunity second received empty"); + + auto nowMs = extractNowSysMs(second, "decryptForCommunity.second.nowMs"); + auto proBackendPubkeyHex = extractProBackendPubkeyHex( + second, "decryptForCommunity.second.proBackendPubkeyHex"); + + std::vector decrypted; + std::vector decryptedServerIds; + + for (uint32_t i = 0; i < first.Length(); i++) { + auto itemValue = first.Get(i); + if (!itemValue.IsObject()) { + throw std::invalid_argument( + "decryptForCommunity itemValue is not an " + "object"); + } + auto obj = itemValue.As(); + + try { + uint32_t serverId = extractServerId(obj, "decryptForCommunity.obj.serverId"); + + auto contentOrEnvelope = + extractContentOrEnvelope(obj, "decryptForCommunity.obj.contentOrEnvelope"); + decrypted.push_back(session::decode_for_community( + contentOrEnvelope, nowMs, proBackendPubkeyHex)); + decryptedServerIds.push_back(serverId); + + } catch (const std::exception& e) { + log::warning( + cat, + "decryptForCommunity: Failed to decrypt " + "message at index {}", + i); + } + } + + auto ret = Napi::Array::New(info.Env(), decrypted.size()); + uint32_t i = 0; + + for (auto& d : decrypted) { + auto to_insert = Napi::Object::New(info.Env()); + std::span content_plaintext_unpadded = + std::span(d.content_plaintext).subspan(0, d.content_plaintext_unpadded_size); + + to_insert.Set( + "envelope", d.envelope ? toJs(info.Env(), *d.envelope) : info.Env().Null()); + to_insert.Set("contentPlaintextUnpadded", toJs(info.Env(), content_plaintext_unpadded)); + to_insert.Set("serverId", toJs(info.Env(), decryptedServerIds[i])); + + to_insert.Set( + "proSigHex", + d.pro_sig ? toJs(info.Env(), oxenc::to_hex(*d.pro_sig)) : info.Env().Null()); + to_insert.Set("decodedPro", d.pro ? toJs(info.Env(), d.pro) : info.Env().Null()); + + ret.Set(i, to_insert); + i++; + } + + return ret; + }); +}; + +Napi::Value MultiEncryptWrapper::decryptFor1o1(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + // we expect two arguments that match: + // first: [{ + // "envelopePayload": Uint8Array, + // "messageHash": string, + // }], + // second: { + // "nowMs": number, + // "proBackendPubkeyHex": Hexstring, + // "ed25519PrivateKeyHex": Hexstring, + // } + // + + assertInfoLength(info, 2); + assertIsArray(info[0], "decryptFor1o1 info[0]"); + assertIsObject(info[1]); + + auto first = info[0].As(); + + if (first.IsEmpty()) + throw std::invalid_argument("decryptFor1o1 first received empty"); + + auto second = info[1].As(); + + if (second.IsEmpty()) + throw std::invalid_argument("decryptFor1o1 second received empty"); + + auto nowMs = extractNowSysMs(second, "decryptFor1o1.second.nowMs"); + auto proBackendPubkeyHex = + extractProBackendPubkeyHex(second, "decryptFor1o1.second.proBackendPubkeyHex"); + + std::vector decrypted; + std::vector decryptedMessageHashes; + + DecodeEnvelopeKey keys{}; + auto keySpan = + extractEd25519PrivateKeyHex(second, "decryptFor1o1.second.ed25519PrivateKeyHex"); + + std::vector> keySpans; + keySpans.emplace_back(keySpan.data(), keySpan.size()); + keys.decrypt_keys = keySpans; + + for (uint32_t i = 0; i < first.Length(); i++) { + auto itemValue = first.Get(i); + if (!itemValue.IsObject()) { + throw std::invalid_argument( + "decryptFor1o1 itemValue is not an " + "object"); + } + auto obj = itemValue.As(); + + try { + std::string messageHash = extractMessageHash(obj, "decryptFor1o1.obj.messageHash"); + + auto envelopePayload = + extractEnvelopePayload(obj, "decryptFor1o1.obj.envelopePayload"); + decrypted.push_back(session::decode_envelope( + keys, envelopePayload, nowMs, proBackendPubkeyHex)); + decryptedMessageHashes.push_back(messageHash); + } catch (const std::exception& e) { + log::warning( + cat, + "decryptFor1o1: Failed to decrypt " + "message at index {}", + i); + } + } + + auto ret = Napi::Array::New(info.Env(), decrypted.size()); + uint32_t i = 0; + + for (auto& d : decrypted) { + auto to_insert = Napi::Object::New(info.Env()); + + to_insert.Set("decodedEnvelope", toJs(info.Env(), d)); + to_insert.Set("messageHash", toJs(info.Env(), decryptedMessageHashes[i])); + + ret.Set(i, to_insert); + i++; + } + + return ret; + }); +}; + +Napi::Value MultiEncryptWrapper::decryptForGroup(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + // we expect two arguments that match: + // first: [{ + // "envelopePayload": Uint8Array, + // "messageHash": string, + // }], + // second: { + // "nowMs": number, + // "proBackendPubkeyHex": Hexstring, + // "ed25519GroupPubkeyHex": Hexstring, + // "groupEncKeys": Array, + // } + // + + assertInfoLength(info, 2); + assertIsArray(info[0], "decryptForGroup info[0]"); + assertIsObject(info[1]); + + auto first = info[0].As(); + + if (first.IsEmpty()) + throw std::invalid_argument("decryptForGroup first received empty"); + + auto second = info[1].As(); + + if (second.IsEmpty()) + throw std::invalid_argument("decryptForGroup second received empty"); + + auto nowMs = extractNowSysMs(second, "decryptForGroup.second.nowMs"); + auto proBackendPubkeyHex = + extractProBackendPubkeyHex(second, "decryptForGroup.second.proBackendPubkeyHex"); + + std::vector decrypted; + std::vector decryptedMessageHashes; + + DecodeEnvelopeKey keys{}; + auto groupPk = extractEd25519GroupPubkeyHex( + second, "decryptForGroup.second.ed25519GroupPubkeyHex"); + + // this has to be vector and not spans, the memory gets freed by the function + std::vector> groupEncKeysVec = + extractGroupEncKeys(second, "decryptForGroup.second.groupEncKeys"); + + std::vector> span_group_enc_keys; + span_group_enc_keys.reserve(span_group_enc_keys.size()); + for (const auto& inner : groupEncKeysVec) { + span_group_enc_keys.emplace_back(inner); + } + + // Create a span of spans + std::span> groupEncKeys(span_group_enc_keys); + + keys.decrypt_keys = groupEncKeys; + + for (uint32_t i = 0; i < first.Length(); i++) { + auto itemValue = first.Get(i); + if (!itemValue.IsObject()) { + throw std::invalid_argument( + "decryptForGroup itemValue is not an " + "object"); + } + auto obj = itemValue.As(); + + try { + std::string messageHash = + extractMessageHash(obj, "decryptForGroup.obj.messageHash"); + + auto envelopePayload = + extractEnvelopePayload(obj, "decryptForGroup.obj.envelopePayload"); + decrypted.push_back(session::decode_envelope( + keys, envelopePayload, nowMs, proBackendPubkeyHex)); + decryptedMessageHashes.push_back(messageHash); + } catch (const std::exception& e) { + log::warning( + cat, + "decryptForGroup: Failed to decrypt " + "message at index {}", + i); + } + } + + auto ret = Napi::Array::New(info.Env(), decrypted.size()); + uint32_t i = 0; + + for (auto& d : decrypted) { + auto to_insert = Napi::Object::New(info.Env()); + + to_insert.Set("decodedEnvelope", toJs(info.Env(), d)); + to_insert.Set("messageHash", toJs(info.Env(), decryptedMessageHashes[i])); + + ret.Set(i, to_insert); + i++; + } + + return ret; + }); +}; + +}; // namespace session::nodeapi diff --git a/src/groups/meta_group_wrapper.cpp b/src/groups/meta_group_wrapper.cpp index 8daa2d5..18f2523 100644 --- a/src/groups/meta_group_wrapper.cpp +++ b/src/groups/meta_group_wrapper.cpp @@ -139,6 +139,8 @@ void MetaGroupWrapper::Init(Napi::Env env, Napi::Object exports) { InstanceMethod("keysNeedsRekey", &MetaGroupWrapper::keysNeedsRekey), InstanceMethod("keyRekey", &MetaGroupWrapper::keyRekey), InstanceMethod("keyGetAll", &MetaGroupWrapper::keyGetAll), + InstanceMethod( + "keyGetEncryptionKeyHex", &MetaGroupWrapper::keyGetEncryptionKeyHex), InstanceMethod("activeHashes", &MetaGroupWrapper::activeHashes), InstanceMethod("loadKeyMessage", &MetaGroupWrapper::loadKeyMessage), InstanceMethod("keyGetCurrentGen", &MetaGroupWrapper::keyGetCurrentGen), @@ -743,6 +745,10 @@ Napi::Value MetaGroupWrapper::keyGetAll(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { return meta_group->keys->group_keys(); }); } +Napi::Value MetaGroupWrapper::keyGetEncryptionKeyHex(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { return to_hex(meta_group->keys->group_enc_key()); }); +} + Napi::Value MetaGroupWrapper::loadKeyMessage(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { assertInfoLength(info, 3); diff --git a/src/meta/meta_base_wrapper.cpp b/src/meta/meta_base_wrapper.cpp new file mode 100644 index 0000000..8369e40 --- /dev/null +++ b/src/meta/meta_base_wrapper.cpp @@ -0,0 +1,91 @@ +#include "meta/meta_base_wrapper.hpp" + +#include + +#include +#include + +#include "groups/meta_group.hpp" + +namespace session::nodeapi { + +std::unique_ptr MetaBaseWrapper::constructGroupWrapper( + const Napi::CallbackInfo& info, const std::string& class_name) { + return wrapExceptions(info, [&] { + if (!info.IsConstructCall()) + throw std::invalid_argument{"You need to call the constructor with the `new` syntax"}; + + assertInfoLength(info, 1); + auto arg = info[0]; + assertIsObject(arg); + auto obj = arg.As(); + + if (obj.IsEmpty()) + throw std::invalid_argument("constructGroupWrapper received empty"); + + assertIsUInt8Array(obj.Get("userEd25519Secretkey"), "constructGroupWrapper userEd"); + auto user_ed25519_secretkey = toCppBuffer( + obj.Get("userEd25519Secretkey"), + class_name + ":constructGroupWrapper.userEd25519Secretkey"); + + assertIsUInt8Array(obj.Get("groupEd25519Pubkey"), "constructGroupWrapper groupEd"); + auto group_ed25519_pubkey = toCppBuffer( + obj.Get("groupEd25519Pubkey"), + class_name + ":constructGroupWrapper.groupEd25519Pubkey"); + + std::optional> group_ed25519_secretkey = maybeNonemptyBuffer( + obj.Get("groupEd25519Secretkey"), + class_name + ":constructGroupWrapper.groupEd25519Secretkey"); + + std::optional> dumped_meta = maybeNonemptyBuffer( + obj.Get("metaDumped"), class_name + ":constructGroupWrapper.metaDumped"); + + std::optional dumped_info; + std::optional dumped_members; + std::optional dumped_keys; + + if (dumped_meta) { + auto dumped_meta_str = to_string(*dumped_meta); + + oxenc::bt_dict_consumer combined{dumped_meta_str}; + // NB: must read in ascii-sorted order: + if (!combined.skip_until("info")) + throw std::runtime_error{"info dump not found in combined dump!"}; + dumped_info = combined.consume_string(); + + if (!combined.skip_until("keys")) + throw std::runtime_error{"keys dump not found in combined dump!"}; + dumped_keys = combined.consume_string(); + + if (!combined.skip_until("members")) + throw std::runtime_error{"members dump not found in combined dump!"}; + dumped_members = combined.consume_string(); + } + + // Note, we keep shared_ptr for those as the Keys one need a reference to Members and + // Info on its own currently. + auto info = std::make_shared( + group_ed25519_pubkey, + group_ed25519_secretkey, + (dumped_info ? std::make_optional(session::to_span(*dumped_info)) : std::nullopt)); + + auto members = std::make_shared( + group_ed25519_pubkey, + group_ed25519_secretkey, + (dumped_members ? std::make_optional(session::to_span(*dumped_members)) + : std::nullopt)); + + auto keys = std::make_shared( + user_ed25519_secretkey, + group_ed25519_pubkey, + group_ed25519_secretkey, + (dumped_keys ? std::make_optional(session::to_span(*dumped_keys)) : std::nullopt), + *info, + *members); + + return std::make_unique( + info, members, keys, group_ed25519_pubkey, group_ed25519_secretkey); + }); +} + +} // namespace session::nodeapi diff --git a/src/pro/pro.cpp b/src/pro/pro.cpp new file mode 100644 index 0000000..37b739f --- /dev/null +++ b/src/pro/pro.cpp @@ -0,0 +1 @@ +#include "pro/pro.hpp" \ No newline at end of file diff --git a/src/pro/types.cpp b/src/pro/types.cpp new file mode 100644 index 0000000..7526da1 --- /dev/null +++ b/src/pro/types.cpp @@ -0,0 +1 @@ +#include "pro/types.hpp" diff --git a/src/user_config.cpp b/src/user_config.cpp index da74ef9..6f0ed29 100644 --- a/src/user_config.cpp +++ b/src/user_config.cpp @@ -3,14 +3,70 @@ #include #include "base_config.hpp" +#include "oxen/log.hpp" +#include "oxenc/hex.h" +#include "pro/types.hpp" #include "profile_pic.hpp" #include "session/config/base.hpp" #include "session/config/user_profile.hpp" +#include "session/ed25519.hpp" namespace session::nodeapi { using config::UserProfile; +session::config::ProConfig pro_config_from_object(Napi::Object input) { + session::config::ProConfig pro_config = {}; + + auto rotating_privkey_hex_js = input.Get("rotatingPrivKeyHex"); + assertIsString(rotating_privkey_hex_js, "pro_config_from_object.rotating_privkey_js"); + auto rotating_privkey_cpp = from_hex_to_vector( + toCppString(rotating_privkey_hex_js, "pro_config_from_object.rotating_privkey_js")); + assert_length(rotating_privkey_cpp, 64, "pro_config_from_object.rotating_privkey_js"); + std::copy( + rotating_privkey_cpp.begin(), + rotating_privkey_cpp.end(), + pro_config.rotating_privkey.begin()); + + auto proProof = input.Get("proProof"); + assertIsObject(proProof); + auto proof_js = proProof.As(); + + // extract version + assertIsNumber(proof_js.Get("version"), "pro_config_from_object.version"); + pro_config.proof.version = + toCppInteger(proof_js.Get("version"), "pro_config_from_object.version"); + // extract genIndexHashB64 + auto gen_index_hash_b64 = proof_js.Get("genIndexHashB64"); + assertIsString(gen_index_hash_b64, "pro_config_from_object.genIndexHashB64"); + auto gen_index_hash_b64_cpp = + toCppString(gen_index_hash_b64, "pro_config_from_object.genIndexHashB64"); + auto gen_index_hash_cpp = from_base64_to_vector(gen_index_hash_b64_cpp); + std::copy( + gen_index_hash_cpp.begin(), + gen_index_hash_cpp.end(), + pro_config.proof.gen_index_hash.begin()); + + // extract rotatingPubkeyHex + auto rotating_pubkey_hex_js = proof_js.Get("rotatingPubkeyHex"); + assertIsString(rotating_pubkey_hex_js, "pro_config_from_object.rotatingPubkeyHex"); + auto rotating_pubkey_hex_cpp = + toCppString(rotating_pubkey_hex_js, "pro_config_from_object.rotatingPubkeyHex"); + auto rotating_pubkey_cpp = from_hex_to_vector(rotating_pubkey_hex_cpp); + assert_length(rotating_pubkey_cpp, 32, "pro_config_from_object.rotatingPubkeyHex"); + std::copy( + rotating_pubkey_cpp.begin(), + rotating_pubkey_cpp.end(), + pro_config.proof.rotating_pubkey.begin()); + + // extract expiryMs + assertIsNumber(proof_js.Get("expiryMs"), "pro_config_from_object.expiryMs"); + pro_config.proof.expiry_unix_ts = + toCppSysMs(proof_js.Get("expiryMs"), "pro_config_from_object.expiryMs"); + + return pro_config; +}; + void UserConfigWrapper::Init(Napi::Env env, Napi::Object exports) { InitHelper( env, @@ -37,6 +93,10 @@ void UserConfigWrapper::Init(Napi::Env env, Napi::Object exports) { &UserConfigWrapper::setEnableBlindedMsgRequest), InstanceMethod("getNoteToSelfExpiry", &UserConfigWrapper::getNoteToSelfExpiry), InstanceMethod("setNoteToSelfExpiry", &UserConfigWrapper::setNoteToSelfExpiry), + InstanceMethod("getProConfig", &UserConfigWrapper::getProConfig), + InstanceMethod("setProConfig", &UserConfigWrapper::setProConfig), + InstanceMethod( + "generateProMasterKey", &UserConfigWrapper::generateProMasterKey), }); } @@ -141,7 +201,6 @@ Napi::Value UserConfigWrapper::getProfileUpdatedSeconds(const Napi::CallbackInfo }); } - Napi::Value UserConfigWrapper::getEnableBlindedMsgRequest(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { auto env = info.Env(); @@ -186,4 +245,49 @@ void UserConfigWrapper::setNoteToSelfExpiry(const Napi::CallbackInfo& info) { }); } +Napi::Value UserConfigWrapper::getProConfig(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + auto pro_config = config.get_pro_config(); + if (pro_config) { + return toJs(info.Env(), *pro_config); + } + + return info.Env().Null(); + }); +} + +void UserConfigWrapper::setProConfig(const Napi::CallbackInfo& info) { + wrapExceptions(info, [&] { + assertInfoLength(info, 1); + auto pro_config_js = info[0]; + assertIsObject(pro_config_js); + + session::config::ProConfig pro_config = + pro_config_from_object(pro_config_js.As()); + + config.set_pro_config(pro_config); + }); +} + +Napi::Value UserConfigWrapper::generateProMasterKey(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 1); + + auto input = info[0]; + assertIsObject(input); + auto ed25519_seed_js_hex = input.As().Get("ed25519SeedHex"); + assertIsString(ed25519_seed_js_hex, "generateProMasterKey"); + auto ed25519_seed_cpp_hex = toCppString(ed25519_seed_js_hex, "generateProMasterKey"); + assert_length(ed25519_seed_cpp_hex, 64, "generateProMasterKey"); + auto converted = from_hex_to_vector(ed25519_seed_cpp_hex); + + auto pro_master_key_hex = + session::ed25519::ed25519_pro_key_pair_for_ed25519_seed(converted); + auto obj = Napi::Object::New(info.Env()); + obj["proMasterKey"] = toJs(info.Env(), pro_master_key_hex); + + return obj; + }); +} + } // namespace session::nodeapi diff --git a/src/utilities.cpp b/src/utilities.cpp index 15bd2f3..d81cb20 100644 --- a/src/utilities.cpp +++ b/src/utilities.cpp @@ -1,5 +1,6 @@ #include "utilities.hpp" +#include #include #include @@ -18,8 +19,10 @@ void assertInfoMinLength(const Napi::CallbackInfo& info, const int minLength) { checkOrThrow(info.Length() < minLength, "Invalid number of min length arguments"); } -void assertIsStringOrNull(const Napi::Value& val) { - checkOrThrow(val.IsString() || val.IsNull(), "Wrong arguments: expected string or null"); +void assertIsStringOrNull(const Napi::Value& val, const std::string& identifier) { + checkOrThrow( + val.IsString() || val.IsNull(), + std::string("Wrong arguments: expected string or null" + identifier).c_str()); } void assertIsNumber(const Napi::Value& val, const std::string& identifier) { @@ -50,15 +53,18 @@ void assertIsUInt8ArrayOrNull(const Napi::Value& val) { void assertIsUInt8Array(const Napi::Value& val, const std::string& identifier) { checkOrThrow( IsUint8Array(val), - std::string("Wrong arguments: expected uint8Array" + identifier).c_str()); + std::string("Wrong arguments: expected uint8Array: " + identifier).c_str()); } -void assertIsString(const Napi::Value& val) { - checkOrThrow(val.IsString(), "Wrong arguments: expected string"); +void assertIsString(const Napi::Value& val, const std::string& identifier) { + checkOrThrow( + val.IsString(), std::string("Wrong arguments: expected string: " + identifier).c_str()); } -void assertIsBoolean(const Napi::Value& val) { - checkOrThrow(val.IsBoolean(), "Wrong arguments: expected boolean"); +void assertIsBoolean(const Napi::Value& val, const std::string& identifier) { + checkOrThrow( + val.IsBoolean(), + std::string("Wrong arguments: expected boolean: " + identifier).c_str()); } std::string toCppString(Napi::Value x, const std::string& identifier) { @@ -163,6 +169,27 @@ std::chrono::sys_seconds toCppSysSeconds(Napi::Value x, const std::string& ident throw std::invalid_argument{"toCppSysSeconds with invalid type, called from " + identifier}; } +std::chrono::sys_time toCppSysMs( + Napi::Value x, const std::string& identifier) { + + if (x.IsNumber()) { + auto num = x.As().Int64Value(); + return std::chrono::sys_time{std::chrono::milliseconds{num}}; + } + + throw std::invalid_argument{"toCppSysMs with invalid type, called from " + identifier}; +} + +std::chrono::milliseconds toCppMs(Napi::Value x, const std::string& identifier) { + + if (x.IsNumber()) { + auto num = x.As().Int64Value(); + return std::chrono::milliseconds{num}; + } + + throw std::invalid_argument{"toCppMs with invalid type, called from " + identifier}; +} + std::optional maybeNonemptyProfilePic( Napi::Value x, const std::string& identifier) { if (x.IsNull() || x.IsUndefined()) @@ -190,7 +217,7 @@ std::optional maybeNonemptyProfilePic( // when the `x` obj is provided (i.e. not null), it should have those 2 fields set. // They can be empty (meaning to remove the profile pic), but not undefined/null, as the // object itself should have been undefined/null - throw new std::invalid_argument{"maybeNonemptyProfilePic with invalid input"}; + throw std::invalid_argument{"maybeNonemptyProfilePic with invalid input"}; } return session::config::profile_pic{*url, *key}; @@ -317,4 +344,56 @@ confirm_pushed_entry_t confirm_pushed_entry_from_JS(const Napi::Env& env, const confirm_pushed_entry_t confirmed_pushed_entry{seqno, hashes}; return confirmed_pushed_entry; } + +Napi::Object proFeaturesToJs(const Napi::Env& env, const SESSION_PROTOCOL_PRO_FEATURES bitset) { + Napi::Array arr = Napi::Array::New(env); + uint32_t index = 0; + + if (bitset == SESSION_PROTOCOL_PRO_FEATURES_NIL) { + return arr; + } + + if (bitset & (SESSION_PROTOCOL_PRO_FEATURES_10K_CHARACTER_LIMIT)) { + arr[index] = Napi::String::New(env, "10K_CHARACTER_LIMIT"); + index++; + } + if (bitset & SESSION_PROTOCOL_PRO_FEATURES_PRO_BADGE) { + arr[index++] = Napi::String::New(env, "PRO_BADGE"); + index++; + } + if (bitset & SESSION_PROTOCOL_PRO_FEATURES_ANIMATED_AVATAR) { + arr[index++] = Napi::String::New(env, "ANIMATED_AVATAR"); + index++; + } + + return arr; +} + +std::span from_hex_to_span(std::string_view x) { + return session::to_span(oxenc::from_hex(x)); +} + +std::vector from_hex_to_vector(std::string_view x) { + return session::to_vector(oxenc::from_hex(x)); +} + +std::span from_base64_to_span(std::string_view x) { + return session::to_span(oxenc::from_base64(x)); +} + +std::vector from_base64_to_vector(std::string_view x) { + return session::to_vector(oxenc::from_base64(x)); +} + +template +std::array spanToArray(std::span span) { + if (span.size() != N) { + throw std::invalid_argument("Span size does not match array size"); + } + + std::array result; + std::ranges::copy(span, result.begin()); + return result; +} + } // namespace session::nodeapi \ No newline at end of file diff --git a/types/groups/groupkeys.d.ts b/types/groups/groupkeys.d.ts index ec5e67a..5b99f88 100644 --- a/types/groups/groupkeys.d.ts +++ b/types/groups/groupkeys.d.ts @@ -6,6 +6,7 @@ declare module 'libsession_util_nodejs' { keysNeedsRekey: () => boolean; keyRekey: () => Uint8Array; keyGetAll: () => Array; + keyGetEncryptionKeyHex: () => string; loadKeyMessage: (hash: string, data: Uint8Array, timestampMs: number) => boolean; keysAdmin: () => boolean; keyGetCurrentGen: () => number; diff --git a/types/groups/metagroup.d.ts b/types/groups/metagroup.d.ts index deed1bd..52aa8c6 100644 --- a/types/groups/metagroup.d.ts +++ b/types/groups/metagroup.d.ts @@ -164,6 +164,7 @@ declare module 'libsession_util_nodejs' { | MakeActionCall | MakeActionCall | MakeActionCall + | MakeActionCall | MakeActionCall | MakeActionCall | MakeActionCall diff --git a/types/index.d.ts b/types/index.d.ts index 2762af0..75303fd 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2,5 +2,6 @@ /// /// /// +/// /// /// diff --git a/types/multi_encrypt/multi_encrypt.d.ts b/types/multi_encrypt/multi_encrypt.d.ts index f7c3fb1..6f489eb 100644 --- a/types/multi_encrypt/multi_encrypt.d.ts +++ b/types/multi_encrypt/multi_encrypt.d.ts @@ -1,6 +1,105 @@ /// +/// declare module 'libsession_util_nodejs' { + type WithEncryptedData = { encryptedData: Uint8Array }; + type WithPlaintext = { plaintext: Uint8Array }; + type WithSentTimestampMs = { + /** in milliseconds */ + sentTimestampMs: number; + }; + type WithSenderEd25519Seed = { + /** + * 32 bytes + */ + senderEd25519Seed: Uint8Array; + }; + type WithRecipientPubkey = { recipientPubkey: string }; + type WithCommunityPubkey = { communityPubkey: string }; + type WithGroupEd25519Pubkey = { groupEd25519Pubkey: string }; + type WithGroupEncKey = { groupEncKey: string }; + type WithEd25519PrivateKeyHex = { + /** + * This is the current user identity private key, HexString + */ + ed25519PrivateKeyHex: string; + }; + type WithEd25519GroupPubkeyHex = { + /** + * This is the group identity pubkey, HexString + */ + ed25519GroupPubkeyHex: string; + }; + type WithGroupEncryptionKeys = { + /** + * This should be what is returned by the 03-group `MetaGroupWrapper::keyGetAll()` + */ + groupEncKeys: Array; + }; + + type WithContentOrEnvelope = { contentOrEnvelope: Uint8Array }; + type WithEnvelopePayload = { envelopePayload: Uint8Array }; + type WithContentPlaintext = { + contentPlaintextUnpadded: Uint8Array; + }; + type WithServerId = { + serverId: number; + }; + type WithMessageHash = { + /** + * Base64 string + */ + messageHash: string; + }; + type WithNowMs = { nowMs: number }; + + type DecodedPro = WithProFeatures & { + proStatus: ProStatus; + proProof: ProProof; + }; + + type WithDecodedPro = { + decodedPro: DecodedPro | null; + }; + type WithProSigHex = { + /** + * HexString + */ + proSigHex: string | null; + }; + + type Envelope = { + timestampMs: number; + /** + * HexString, 33 bytes, 66 chars + */ + source: string | null; + /** + * HexString + */ + proSigHex: string | null; + }; + + type WithEnvelope = { + envelope: Envelope | null; + }; + + type WithNonNullableEnvelope = { + envelope: Envelope; + }; + + type WithDecodedEnvelope = { + decodedEnvelope: + | WithNonNullableEnvelope & + WithContentPlaintext & + WithDecodedPro & { + /** + * HexString with 05 prefix of the author of that message + */ + sessionId: string; + }; + }; + type MultiEncryptWrapper = { multiEncrypt: (opts: { /** @@ -28,14 +127,65 @@ declare module 'libsession_util_nodejs' { data: Uint8Array; domain: 'attachment' | 'profilePic'; allowLarge: boolean; - }) => { encryptedData: Uint8Array; encryptionKey: Uint8Array }; + }) => WithEncryptedData & { encryptionKey: Uint8Array }; /** * * Throws if the decryption fails */ - attachmentDecrypt: (opts: { encryptedData: Uint8Array; decryptionKey: Uint8Array }) => { + attachmentDecrypt: (opts: WithEncryptedData & { decryptionKey: Uint8Array }) => { decryptedData: Uint8Array; }; + + encryptFor1o1: ( + opts: Array< + WithPlaintext & + WithSentTimestampMs & + WithSenderEd25519Seed & + WithRecipientPubkey & + WithProRotatingEd25519PrivKey + > + ) => { encryptedData: Array }; + + encryptForCommunityInbox: ( + opts: Array< + WithPlaintext & + WithSentTimestampMs & + WithSenderEd25519Seed & + WithRecipientPubkey & + WithCommunityPubkey & + WithProRotatingEd25519PrivKey + > + ) => { encryptedData: Array }; + + encryptForCommunity: (opts: Array) => { + encryptedData: Array; + }; + + encryptForGroup: ( + opts: Array< + WithPlaintext & + WithSenderEd25519Seed & + WithSentTimestampMs & + WithGroupEd25519Pubkey & + WithGroupEncKey & + WithProRotatingEd25519PrivKey + > + ) => { encryptedData: Array }; + + decryptForCommunity: ( + first: Array, + second: WithNowMs & WithProBackendPubkey + ) => Array; + + decryptFor1o1: ( + first: Array, + second: WithNowMs & WithProBackendPubkey & WithEd25519PrivateKeyHex + ) => Array; + + decryptForGroup: ( + first: Array, + second: WithNowMs & WithProBackendPubkey & WithEd25519GroupPubkeyHex & WithGroupEncryptionKeys + ) => Array; }; export type MultiEncryptActionsCalls = MakeWrapperActionCalls; @@ -48,6 +198,14 @@ declare module 'libsession_util_nodejs' { public static multiDecryptEd25519: MultiEncryptWrapper['multiDecryptEd25519']; public static attachmentDecrypt: MultiEncryptWrapper['attachmentDecrypt']; public static attachmentEncrypt: MultiEncryptWrapper['attachmentEncrypt']; + public static encryptFor1o1: MultiEncryptWrapper['encryptFor1o1']; + public static encryptForCommunityInbox: MultiEncryptWrapper['encryptForCommunityInbox']; + public static encryptForCommunity: MultiEncryptWrapper['encryptForCommunity']; + public static encryptForGroup: MultiEncryptWrapper['encryptForGroup']; + + public static decryptForCommunity: MultiEncryptWrapper['decryptForCommunity']; + public static decryptFor1o1: MultiEncryptWrapper['decryptFor1o1']; + public static decryptForGroup: MultiEncryptWrapper['decryptForGroup']; } /** @@ -59,5 +217,12 @@ declare module 'libsession_util_nodejs' { | MakeActionCall | MakeActionCall | MakeActionCall - | MakeActionCall; + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall; } diff --git a/types/pro/index.d.ts b/types/pro/index.d.ts new file mode 100644 index 0000000..921fccf --- /dev/null +++ b/types/pro/index.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts new file mode 100644 index 0000000..806c15c --- /dev/null +++ b/types/pro/pro.d.ts @@ -0,0 +1,102 @@ +/// + +declare module 'libsession_util_nodejs' { + type WithProRotatingEd25519PrivKey = { proRotatingEd25519PrivKey: string | null }; + + type WithProBackendPubkey = { + /** + * HexString + */ + proBackendPubkeyHex: string; + }; + + type ProStatus = 'InvalidProBackendSig' | 'InvalidUserSig' | 'Valid' | 'Expired'; + type ProFeature = '10K_CHARACTER_LIMIT' | 'PRO_BADGE' | 'ANIMATED_AVATAR'; + type ProFeatures = Array; + type WithProFeatures = { proFeatures: ProFeatures }; + + type ProProof = { + version: number; + genIndexHashB64: string; + /** + * HexString, 64 chars + */ + rotatingPubkeyHex: string; + expiryMs: number; + }; + + type ProConfig = { + /** + * 64 bytes, 128 chars + */ + rotatingPrivKeyHex: string; + proProof: ProProof; + }; + + // Must match session-desktop + export enum ProOriginatingPlatform { + Nil = 'Nil', + GooglePlayStore = 'Google', + iOSAppStore = 'iOS', + } + + export type ProBackendProviderConstantType = { + device: string; + store: string; + store_other: string; + platform: string; + platform_account: string; + refund_url: string; + refund_after_platform_deadline_url: string; + update_subscription_url: string; + cancel_subscription_url: string; + }; + + export type ProBackendProviderConstantsType = Record< + ProOriginatingPlatform, + ProBackendProviderConstantType + >; + + export type ProBackendUrlsType = { + roadmap: string; + privacy_policy: string; + terms_of_service: string; + pro_access_not_found: string; + support_url: string; + }; + + type ProWrapper = { + proFeaturesForMessage: (args: { + utf16: string; + /** + * If the utf16 requires 10K_CHARACTER_LIMIT to be set, it will be set in the return. + * If provided (here) as an input, it will be ignored. + */ + proFeatures: ProFeatures; + }) => WithProFeatures & { success: boolean; error: string | null; codepointCount: number }; + proProofRequestBody: (args: { + requestVersion: string, + masterPrivkey: Uint8Array, + rotatingPrivkey: Uint8Array, + unixTsMs: number, + } + ) => string; + }; + + export type ProActionsCalls = MakeWrapperActionCalls; + + + /** + * To be used inside the web worker only (calls are synchronous and won't work asynchronously) + */ + export class ProWrapperNode { + public static proFeaturesForMessage: ProWrapper['proFeaturesForMessage']; + } + + /** + * Those actions are used internally for the web worker communication. + * You should never need to import them in Session directly + * You will need to add an entry here if you add a new function + */ + export type ProActionsType = MakeActionCall; +} diff --git a/types/shared.d.ts b/types/shared.d.ts index 50b06d4..b8b758d 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -1,3 +1,5 @@ +/// + declare module 'libsession_util_nodejs' { type Uint8ArrayFixedLength = { buffer: Uint8Array; @@ -174,6 +176,10 @@ declare module 'libsession_util_nodejs' { * A string corresponding to the full hash of the commit */ LIBSESSION_NODEJS_COMMIT: string; + /** Object containing pro urls **/ + LIBSESSION_PRO_URLS: ProBackendUrlsType; + /** Object containing mapped provder constants */ + LIBSESSION_PRO_PROVIDERS: ProBackendProviderConstantsType; }; export const CONSTANTS: ConstantsType; diff --git a/types/user/userconfig.d.ts b/types/user/userconfig.d.ts index f1ed3d0..0070557 100644 --- a/types/user/userconfig.d.ts +++ b/types/user/userconfig.d.ts @@ -1,4 +1,5 @@ /// +/// declare module 'libsession_util_nodejs' { /** @@ -36,6 +37,22 @@ declare module 'libsession_util_nodejs' { * @returns the expiry in seconds, 0 if off, undefined if not set */ getNoteToSelfExpiry: () => number | undefined; + + setProConfig: (proConfig: ProConfig) => void; + getProConfig: () => ProConfig | null; + generateProMasterKey: ({ + ed25519SeedHex, + }: { + /** + * HexString, 64 chars + */ + ed25519SeedHex: string; + }) => { + /** + * 64 bytes + */ + proMasterKey: Uint8Array; + }; }; export type UserConfigWrapperActionsCalls = MakeWrapperActionCalls; @@ -58,6 +75,9 @@ declare module 'libsession_util_nodejs' { public setEnableBlindedMsgRequest: UserConfigWrapper['setEnableBlindedMsgRequest']; public getNoteToSelfExpiry: UserConfigWrapper['getNoteToSelfExpiry']; public setNoteToSelfExpiry: UserConfigWrapper['setNoteToSelfExpiry']; + public getProConfig: UserConfigWrapper['getProConfig']; + public setProConfig: UserConfigWrapper['setProConfig']; + public generateProMasterKey: UserConfigWrapper['generateProMasterKey']; } /** @@ -80,5 +100,8 @@ declare module 'libsession_util_nodejs' { | MakeActionCall | MakeActionCall | MakeActionCall - | MakeActionCall; + | MakeActionCall + | MakeActionCall + | MakeActionCall + | MakeActionCall; }