From 60790305ae91dcd3a6cfefd62b9d0e599beb7105 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 12 Nov 2025 16:03:45 +1100 Subject: [PATCH 1/5] feat: add bitset for proFeaturesOfMessage & rotatingKeyGeneration --- include/pro/pro.hpp | 38 +++++++------------ include/pro/types.hpp | 2 +- include/user_config.hpp | 2 + include/utilities.hpp | 22 ++++++----- libsession-util | 2 +- src/encrypt_decrypt/encrypt_decrypt.cpp | 19 +++++----- src/user_config.cpp | 28 +++++++++++--- src/utilities.cpp | 39 +++++++------------ types/multi_encrypt/multi_encrypt.d.ts | 2 +- types/pro/pro.d.ts | 50 ++++--------------------- types/user/userconfig.d.ts | 15 ++++++-- 11 files changed, 96 insertions(+), 123 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index 8859e39..80e60c3 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -135,7 +136,7 @@ class ProWrapper : public Napi::ObjectWrap { // we expect two arguments that match: // first: { // "utf16": string, - // "proFeatures": Array, + // "proFeaturesBitset": bigint, // } assertInfoLength(info, 1); @@ -147,27 +148,13 @@ class ProWrapper : public Napi::ObjectWrap { 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; - } - } + assertIsBigint( + first.Get("proFeaturesBitset"), "proFeaturesForMessage.proFeaturesBitset"); + + auto lossless = true; + SESSION_PROTOCOL_PRO_FEATURES flags = + first.Get("proFeaturesBitset").As().Uint64Value(&lossless); + assertIsString(first.Get("utf16"), "proFeaturesForMessage.utf16"); std::u16string utf16 = first.Get("utf16").As().Utf16Value(); auto pro_features_msg = @@ -179,7 +166,7 @@ class ProWrapper : public Napi::ObjectWrap { 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); + obj["proFeaturesBitset"] = proFeaturesToJsBitset(env, pro_features_msg.features); return obj; }); @@ -355,11 +342,12 @@ class ProWrapper : public Napi::ObjectWrap { auto master_privkey = toCppString(master_privkey_js, "proStatusRequestBody.masterPrivKeyHex"); - assert_length(master_privkey, 64, "proStatusRequestBody.masterPrivKeyHex"); + auto master_privkey_decoded = from_hex(master_privkey); + assert_length(master_privkey_decoded, 64, "proStatusRequestBody.masterPrivKeyHex"); auto json = pro_backend::GetProStatusRequest::build_to_json( static_cast(requestVersion.Int32Value()), - to_span(from_hex(master_privkey)), + to_span(master_privkey_decoded), unix_ts_ms, withPaymentHistory); diff --git a/include/pro/types.hpp b/include/pro/types.hpp index a8ce087..7e2e36e 100644 --- a/include/pro/types.hpp +++ b/include/pro/types.hpp @@ -77,7 +77,7 @@ struct toJs_impl { : decoded_pro.status == ProStatus::Valid ? "Valid" : "Expired"); obj["proProof"] = toJs(env, decoded_pro.proof); - obj["proFeatures"] = proFeaturesToJs(env, decoded_pro.features); + obj["proFeaturesBitset"] = proFeaturesToJsBitset(env, decoded_pro.features); return obj; } diff --git a/include/user_config.hpp b/include/user_config.hpp index ba4ca5d..58665b8 100644 --- a/include/user_config.hpp +++ b/include/user_config.hpp @@ -38,5 +38,7 @@ class UserConfigWrapper : public ConfigBaseImpl, public Napi::ObjectWrap from_hex_to_span(std::string_view x); @@ -405,10 +407,11 @@ 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(fmt::format( - "from_hex_to_array: Decoded hex size mismatch: expected {}, got {}", - N, - as_hex.size())); + throw std::invalid_argument( + fmt::format( + "from_hex_to_array: Decoded hex size mismatch: expected {}, got {}", + N, + as_hex.size())); } std::array result; @@ -424,16 +427,15 @@ 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; + {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(fmt::format( - "assert_length: expected {}, got {} for {}", n, x.size(), base_identifier)); + throw std::invalid_argument( + fmt::format( + "assert_length: expected {}, got {} for {}", n, x.size(), base_identifier)); } } diff --git a/libsession-util b/libsession-util index 32bda61..0fd3d5c 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 32bda61d0a108841c773a02271ad8dd377eba3c7 +Subproject commit 0fd3d5cc07704b375f0185c5fb49ad9b5cfb8756 diff --git a/src/encrypt_decrypt/encrypt_decrypt.cpp b/src/encrypt_decrypt/encrypt_decrypt.cpp index 753ee6e..dea5220 100644 --- a/src/encrypt_decrypt/encrypt_decrypt.cpp +++ b/src/encrypt_decrypt/encrypt_decrypt.cpp @@ -636,8 +636,9 @@ Napi::Value MultiEncryptWrapper::decryptForCommunity(const Napi::CallbackInfo& i auto contentOrEnvelope = extractContentOrEnvelope(obj, "decryptForCommunity.obj.contentOrEnvelope"); - decrypted.push_back(session::decode_for_community( - contentOrEnvelope, nowMs, proBackendPubkeyHex)); + decrypted.push_back( + session::decode_for_community( + contentOrEnvelope, nowMs, proBackendPubkeyHex)); decryptedServerIds.push_back(serverId); } catch (const std::exception& e) { @@ -654,12 +655,10 @@ Napi::Value MultiEncryptWrapper::decryptForCommunity(const Napi::CallbackInfo& i 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("contentPlaintextUnpadded", toJs(info.Env(), d.content_plaintext)); to_insert.Set("serverId", toJs(info.Env(), decryptedServerIds[i])); to_insert.Set( @@ -732,8 +731,9 @@ Napi::Value MultiEncryptWrapper::decryptFor1o1(const Napi::CallbackInfo& info) { auto envelopePayload = extractEnvelopePayload(obj, "decryptFor1o1.obj.envelopePayload"); - decrypted.push_back(session::decode_envelope( - keys, envelopePayload, nowMs, proBackendPubkeyHex)); + decrypted.push_back( + session::decode_envelope( + keys, envelopePayload, nowMs, proBackendPubkeyHex)); decryptedMessageHashes.push_back(messageHash); } catch (const std::exception& e) { log::warning( @@ -831,8 +831,9 @@ Napi::Value MultiEncryptWrapper::decryptForGroup(const Napi::CallbackInfo& info) auto envelopePayload = extractEnvelopePayload(obj, "decryptForGroup.obj.envelopePayload"); - decrypted.push_back(session::decode_envelope( - keys, envelopePayload, nowMs, proBackendPubkeyHex)); + decrypted.push_back( + session::decode_envelope( + keys, envelopePayload, nowMs, proBackendPubkeyHex)); decryptedMessageHashes.push_back(messageHash); } catch (const std::exception& e) { log::warning( diff --git a/src/user_config.cpp b/src/user_config.cpp index aafe90b..0f4bd2c 100644 --- a/src/user_config.cpp +++ b/src/user_config.cpp @@ -247,10 +247,11 @@ 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); - } + // TODO fixme once extra_data is implemented + // auto pro_config = config.get_pro_config(); + // if (pro_config) { + // return toJs(info.Env(), *pro_config); + // } return info.Env().Null(); }); @@ -264,8 +265,9 @@ void UserConfigWrapper::setProConfig(const Napi::CallbackInfo& info) { session::config::ProConfig pro_config = pro_config_from_object(pro_config_js.As()); + // TODO fixme once extra_data is implemented - config.set_pro_config(pro_config); + // config.set_pro_config(pro_config); }); } @@ -283,7 +285,21 @@ Napi::Value UserConfigWrapper::generateProMasterKey(const Napi::CallbackInfo& in auto pro_master_key_hex = session::ed25519::ed25519_pro_privkey_for_ed25519_seed(converted); auto obj = Napi::Object::New(info.Env()); - obj["proMasterKey"] = toJs(info.Env(), pro_master_key_hex); + obj["proMasterKeyHex"] = toJs(info.Env(), to_hex(pro_master_key_hex)); + + return obj; + }); +} + +Napi::Value UserConfigWrapper::generateRotatingPrivKeyHex(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + assertInfoLength(info, 0); + auto result = session::ed25519::ed25519_key_pair(); + auto [ed_pk, ed_sk] = result; + + std::string rotating_privkey_hex = to_hex(ed_sk); + auto obj = Napi::Object::New(info.Env()); + obj["rotatingPrivKeyHex"] = toJs(info.Env(), rotating_privkey_hex); return obj; }); diff --git a/src/utilities.cpp b/src/utilities.cpp index d81cb20..aa94b23 100644 --- a/src/utilities.cpp +++ b/src/utilities.cpp @@ -1,5 +1,6 @@ #include "utilities.hpp" +#include #include #include @@ -22,18 +23,24 @@ void assertInfoMinLength(const Napi::CallbackInfo& info, const int minLength) { 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()); + std::string("Wrong arguments: expected string or null: " + identifier).c_str()); } void assertIsNumber(const Napi::Value& val, const std::string& identifier) { checkOrThrow( val.IsNumber() && !val.IsEmpty() && !val.IsNull() && !val.IsUndefined(), - std::string("Wrong arguments: expected number" + identifier).c_str()); + std::string("Wrong arguments: expected number: " + identifier).c_str()); +} + +void assertIsBigint(const Napi::Value& val, const std::string& identifier) { + checkOrThrow( + val.IsBigInt() && !val.IsEmpty() && !val.IsNull() && !val.IsUndefined(), + std::string("Wrong arguments: expected bigint: " + identifier).c_str()); } void assertIsArray(const Napi::Value& val, const std::string& identifier) { checkOrThrow( - val.IsArray(), std::string("Wrong arguments: expected array:" + identifier).c_str()); + val.IsArray(), std::string("Wrong arguments: expected array: " + identifier).c_str()); } void assertIsObject(const Napi::Value& val) { @@ -345,28 +352,10 @@ confirm_pushed_entry_t confirm_pushed_entry_from_JS(const Napi::Env& env, const 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; +Napi::BigInt proFeaturesToJsBitset( + const Napi::Env& env, const SESSION_PROTOCOL_PRO_FEATURES bitset) { + // 2^53 should be enough for now. If we do need more we can use a bigint + return Napi::BigInt::New(env, bitset); } std::span from_hex_to_span(std::string_view x) { diff --git a/types/multi_encrypt/multi_encrypt.d.ts b/types/multi_encrypt/multi_encrypt.d.ts index 6f489eb..aa791cb 100644 --- a/types/multi_encrypt/multi_encrypt.d.ts +++ b/types/multi_encrypt/multi_encrypt.d.ts @@ -53,7 +53,7 @@ declare module 'libsession_util_nodejs' { }; type WithNowMs = { nowMs: number }; - type DecodedPro = WithProFeatures & { + type DecodedPro = WithProFeaturesBitset & { proStatus: ProStatus; proProof: ProProof; }; diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts index ac256d3..2823ab0 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -11,12 +11,11 @@ declare module 'libsession_util_nodejs' { }; type ProStatus = 'InvalidProBackendSig' | 'InvalidUserSig' | 'Valid' | 'Expired'; - type ProFeature = '10K_CHARACTER_LIMIT' | 'PRO_BADGE' | 'ANIMATED_AVATAR'; - type ProFeatures = Array; - type WithProFeatures = { proFeatures: ProFeatures }; + type WithProFeaturesBitset = { proFeaturesBitset: bigint }; type WithGenIndexHash = { genIndexHashB64: string }; type WithRequestVersion = { requestVersion: number }; + type WithTicket = { ticket: number }; type WithUnixTsMs = { unixTsMs: number; @@ -42,11 +41,6 @@ declare module 'libsession_util_nodejs' { proProof: ProProof; }; - // type WithProBackendResponse = { - // status: number; - // errors: Array; - // }; - export type ProOriginatingPlatform = 'Nil' | 'Google' | 'iOS'; export type ProBackendProviderConstantType = { @@ -158,36 +152,24 @@ declare module 'libsession_util_nodejs' { }; 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 & { + proFeaturesForMessage: ( + args: WithProFeaturesBitset & { + utf16: string; + } + ) => WithProFeaturesBitset & { status: 'SUCCESS' | 'UTF_DECODING_ERROR' | 'EXCEEDS_CHARACTER_LIMIT'; }; proProofRequestBody: ( args: WithMasterPrivKeyHex & WithRequestVersion & WithUnixTsMs & WithRotatingPrivKeyHex ) => string; - // proProofParseResponse: (args: { - // json: string; - // }) => WithProBackendResponse & { proof: ProProof | null }; - /** * @param version: Request version. The latest accepted version is 0 * @param ticket: 4-byte monotonic integer for the caller's revocation list iteration. Set to 0 if unknown; otherwise, use the latest known `ticket` from a prior `GetProRevocationsResponse` to allow the Session Pro Backend to omit the revocation list if it has not changed. * @returns the stringified body to include in the request */ - proRevocationsRequestBody: (args: WithRequestVersion & { ticket: number }) => string; - - // proRevocationsParseResponse: (args: { json: string }) => WithProBackendResponse & { - // ticket: number | null; - // items: Array | null; - // }; + proRevocationsRequestBody: (args: WithRequestVersion & WithTicket) => string; proStatusRequestBody: ( args: WithMasterPrivKeyHex & @@ -196,16 +178,6 @@ declare module 'libsession_util_nodejs' { withPaymentHistory: boolean; } ) => string; - - // proStatusParseResponse: (args: { json: string }) => WithProBackendResponse & { - // ticket: number | null; - // items: Array; - // userStatus: number; - // errorReport: number; - // autoRenewing: boolean; - // expiryUnixTsMs: number; - // gracePeriodDurationMs: number; - // }; }; export type ProActionsCalls = MakeWrapperActionCalls; @@ -218,9 +190,6 @@ declare module 'libsession_util_nodejs' { public static proProofRequestBody: ProWrapper['proProofRequestBody']; public static proRevocationsRequestBody: ProWrapper['proRevocationsRequestBody']; public static proStatusRequestBody: ProWrapper['proStatusRequestBody']; - // public static proProofParseResponse: ProWrapper['proProofParseResponse']; - // public static proRevocationsParseResponse: ProWrapper['proRevocationsParseResponse']; - // public static proStatusParseResponse: ProWrapper['proStatusParseResponse']; } /** @@ -233,7 +202,4 @@ declare module 'libsession_util_nodejs' { | MakeActionCall | MakeActionCall | MakeActionCall; - // | MakeActionCall - // | MakeActionCall - // | MakeActionCall } diff --git a/types/user/userconfig.d.ts b/types/user/userconfig.d.ts index 0070557..a8e0f4c 100644 --- a/types/user/userconfig.d.ts +++ b/types/user/userconfig.d.ts @@ -40,6 +40,7 @@ declare module 'libsession_util_nodejs' { setProConfig: (proConfig: ProConfig) => void; getProConfig: () => ProConfig | null; + generateProMasterKey: ({ ed25519SeedHex, }: { @@ -49,10 +50,16 @@ declare module 'libsession_util_nodejs' { ed25519SeedHex: string; }) => { /** - * 64 bytes + * 64 bytes, 128 chars */ - proMasterKey: Uint8Array; + proMasterKeyHex: string; }; + + /** + * Generates a new rotating private key for the user. + * Note: this should only be done once per device, and saved to the DB or the extra_data of `UserProfile`. + */ + generateRotatingPrivKeyHex: () => WithRotatingPrivKeyHex; }; export type UserConfigWrapperActionsCalls = MakeWrapperActionCalls; @@ -78,6 +85,7 @@ declare module 'libsession_util_nodejs' { public getProConfig: UserConfigWrapper['getProConfig']; public setProConfig: UserConfigWrapper['setProConfig']; public generateProMasterKey: UserConfigWrapper['generateProMasterKey']; + public generateRotatingPrivKeyHex: UserConfigWrapper['generateRotatingPrivKeyHex']; } /** @@ -103,5 +111,6 @@ declare module 'libsession_util_nodejs' { | MakeActionCall | MakeActionCall | MakeActionCall - | MakeActionCall; + | MakeActionCall + | MakeActionCall; } From 49943d42b2c9ca6ee188c758949a6e1508c8591d Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 12 Nov 2025 16:25:10 +1100 Subject: [PATCH 2/5] fix: pr review & unit test fixes --- include/pro/pro.hpp | 2 +- src/utilities.cpp | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index 80e60c3..12cfb4a 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -343,7 +343,7 @@ class ProWrapper : public Napi::ObjectWrap { toCppString(master_privkey_js, "proStatusRequestBody.masterPrivKeyHex"); auto master_privkey_decoded = from_hex(master_privkey); - assert_length(master_privkey_decoded, 64, "proStatusRequestBody.masterPrivKeyHex"); + assert_length(master_privkey_decoded, 32, "proStatusRequestBody.masterPrivKeyHex"); auto json = pro_backend::GetProStatusRequest::build_to_json( static_cast(requestVersion.Int32Value()), diff --git a/src/utilities.cpp b/src/utilities.cpp index aa94b23..ddfb230 100644 --- a/src/utilities.cpp +++ b/src/utilities.cpp @@ -354,7 +354,6 @@ confirm_pushed_entry_t confirm_pushed_entry_from_JS(const Napi::Env& env, const Napi::BigInt proFeaturesToJsBitset( const Napi::Env& env, const SESSION_PROTOCOL_PRO_FEATURES bitset) { - // 2^53 should be enough for now. If we do need more we can use a bigint return Napi::BigInt::New(env, bitset); } From 4c142377bcfb158f667a6714fbdc68759d7c17df Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 13 Nov 2025 08:57:30 +1100 Subject: [PATCH 3/5] chore: expose back generateRotatingPrivKeyHex --- src/user_config.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/user_config.cpp b/src/user_config.cpp index 0f4bd2c..8bb2003 100644 --- a/src/user_config.cpp +++ b/src/user_config.cpp @@ -97,6 +97,9 @@ void UserConfigWrapper::Init(Napi::Env env, Napi::Object exports) { InstanceMethod("setProConfig", &UserConfigWrapper::setProConfig), InstanceMethod( "generateProMasterKey", &UserConfigWrapper::generateProMasterKey), + InstanceMethod( + "generateRotatingPrivKeyHex", + &UserConfigWrapper::generateRotatingPrivKeyHex), }); } From 20a3c4fc8847a473b11a13a1931fb8483c1f21a1 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 13 Nov 2025 09:26:22 +1100 Subject: [PATCH 4/5] fix: assert correct length of masterPrivKey --- include/pro/pro.hpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index 12cfb4a..33031d5 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -206,12 +206,13 @@ class ProWrapper : public Napi::ObjectWrap { std::string rotating_privkey = toCppString(rotating_privkey_js, "proProofRequestBody.rotatingPrivKeyHex"); - assert_length(master_privkey, 64, "masterPrivKeyHex"); - assert_length(rotating_privkey, 64, "rotatingPrivkey"); auto master_privkey_decoded = from_hex(master_privkey); auto rotating_privkey_decoded = from_hex(rotating_privkey); + assert_length(master_privkey_decoded, 64, "masterPrivKeyHex"); + assert_length(rotating_privkey_decoded, 64, "rotatingPrivkey"); + std::string json = pro_backend::GetProProofRequest::build_to_json( static_cast(requestVersion.Int32Value()), to_span(master_privkey_decoded), @@ -343,7 +344,7 @@ class ProWrapper : public Napi::ObjectWrap { toCppString(master_privkey_js, "proStatusRequestBody.masterPrivKeyHex"); auto master_privkey_decoded = from_hex(master_privkey); - assert_length(master_privkey_decoded, 32, "proStatusRequestBody.masterPrivKeyHex"); + assert_length(master_privkey_decoded, 64, "proStatusRequestBody.masterPrivKeyHex"); auto json = pro_backend::GetProStatusRequest::build_to_json( static_cast(requestVersion.Int32Value()), From 27285d19d54c151348596e6ea44ca25c828af8de Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 14 Nov 2025 11:03:13 +1100 Subject: [PATCH 5/5] fix: add mocking of tracked proFeaturesBitset to userprofile --- include/pro/pro.hpp | 115 ------------------------ include/pro/types.hpp | 1 + include/user_config.hpp | 9 +- include/utilities.hpp | 6 ++ src/encrypt_decrypt/encrypt_decrypt.cpp | 2 +- src/user_config.cpp | 54 ++++++++++- src/utilities.cpp | 10 +++ types/pro/pro.d.ts | 6 ++ types/user/userconfig.d.ts | 11 +++ 9 files changed, 93 insertions(+), 121 deletions(-) diff --git a/include/pro/pro.hpp b/include/pro/pro.hpp index 33031d5..b245436 100644 --- a/include/pro/pro.hpp +++ b/include/pro/pro.hpp @@ -111,22 +111,6 @@ class ProWrapper : public Napi::ObjectWrap { "proStatusRequestBody", static_cast( napi_writable | napi_configurable)), - - // Note: those are not plugged in for now as we do this parsing through zod - // on desktop. - // Pro responses parsing - // StaticMethod<&ProWrapper::proProofParseResponse>( - // "proProofParseResponse", - // static_cast( - // napi_writable | napi_configurable)), - // StaticMethod<&ProWrapper::proRevocationsParseResponse>( - // "proRevocationsParseResponse", - // static_cast( - // napi_writable | napi_configurable)), - // StaticMethod<&ProWrapper::proStatusParseResponse>( - // "proStatusParseResponse", - // static_cast( - // napi_writable | napi_configurable)), }); } @@ -206,13 +190,9 @@ class ProWrapper : public Napi::ObjectWrap { std::string rotating_privkey = toCppString(rotating_privkey_js, "proProofRequestBody.rotatingPrivKeyHex"); - auto master_privkey_decoded = from_hex(master_privkey); auto rotating_privkey_decoded = from_hex(rotating_privkey); - assert_length(master_privkey_decoded, 64, "masterPrivKeyHex"); - assert_length(rotating_privkey_decoded, 64, "rotatingPrivkey"); - std::string json = pro_backend::GetProProofRequest::build_to_json( static_cast(requestVersion.Int32Value()), to_span(master_privkey_decoded), @@ -223,33 +203,6 @@ class ProWrapper : public Napi::ObjectWrap { }); }; - static Napi::Value proProofParseResponse(const Napi::CallbackInfo& info) { - return wrapResult(info, [&] { - // we expect arguments that match: - // first: { - // "json": string, - // } - - assertInfoLength(info, 1); - assertIsObject(info[0]); - auto env = info.Env(); - - auto first = info[0].As(); - - if (first.IsEmpty()) - throw std::invalid_argument("proProofParseResponse first received empty"); - - assertIsString(first.Get("json"), "proProofParseResponse.json"); - auto json_str = toCppString(first.Get("json"), "proProofParseResponse.json"); - auto parsed = pro_backend::AddProPaymentOrGetProProofResponse::parse(json_str); - - auto obj = toJs(env, static_cast(parsed)); - obj["proof"] = toJsOrNullIfErrors(env, parsed.proof, parsed.errors); - - return obj; - }); - }; - static Napi::Value proRevocationsRequestBody(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { // we expect arguments that match: @@ -281,35 +234,6 @@ class ProWrapper : public Napi::ObjectWrap { }); }; - static Napi::Value proRevocationsParseResponse(const Napi::CallbackInfo& info) { - return wrapResult(info, [&] { - // we expect arguments that match: - // first: { - // "json": string, - // } - - assertInfoLength(info, 1); - assertIsObject(info[0]); - auto env = info.Env(); - - auto first = info[0].As(); - - if (first.IsEmpty()) - throw std::invalid_argument("proRevocationsParseResponse first received empty"); - - assertIsString(first.Get("json"), "proRevocationsParseResponse.json"); - auto json_str = toCppString(first.Get("json"), "proRevocationsParseResponse.json"); - auto parsed = pro_backend::GetProRevocationsResponse::parse(json_str); - - auto obj = toJs(env, static_cast(parsed)); - // if error is set, the body might not be parsable so don't try to use it - obj["ticket"] = parsed.errors.size() ? env.Null() : toJs(env, parsed.ticket); - obj["items"] = parsed.errors.size() ? env.Null() : toJs(env, parsed.items); - - return obj; - }); - }; - static Napi::Value proStatusRequestBody(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { // we expect arguments that match: @@ -344,7 +268,6 @@ class ProWrapper : public Napi::ObjectWrap { toCppString(master_privkey_js, "proStatusRequestBody.masterPrivKeyHex"); auto master_privkey_decoded = from_hex(master_privkey); - assert_length(master_privkey_decoded, 64, "proStatusRequestBody.masterPrivKeyHex"); auto json = pro_backend::GetProStatusRequest::build_to_json( static_cast(requestVersion.Int32Value()), @@ -355,44 +278,6 @@ class ProWrapper : public Napi::ObjectWrap { return json; }); }; - - static Napi::Value proStatusParseResponse(const Napi::CallbackInfo& info) { - return wrapResult(info, [&] { - // we expect arguments that match: - // first: { - // "json": string, - // } - - assertInfoLength(info, 1); - assertIsObject(info[0]); - auto env = info.Env(); - - auto first = info[0].As(); - - if (first.IsEmpty()) - throw std::invalid_argument("proStatusParseResponse first received empty"); - - assertIsString(first.Get("json"), "proStatusParseResponse.json"); - auto json_str = toCppString(first.Get("json"), "proStatusParseResponse.json"); - auto parsed = pro_backend::GetProStatusResponse::parse(json_str); - - auto obj = toJs(env, static_cast(parsed)); - - obj["items"] = toJsOrNullIfErrors(env, parsed.items, parsed.errors); - obj["userStatus"] = toJsOrNullIfErrors( - env, proBackendEnumToString(parsed.user_status), parsed.errors); - - obj["errorReport"] = toJsOrNullIfErrors( - env, proBackendEnumToString(parsed.error_report), parsed.errors); - - obj["autoRenewing"] = toJsOrNullIfErrors(env, parsed.auto_renewing, parsed.errors); - obj["expiryTsMs"] = toJsOrNullIfErrors(env, parsed.expiry_unix_ts_ms, parsed.errors); - obj["gracePeriodMs"] = - toJsOrNullIfErrors(env, parsed.grace_period_duration_ms, parsed.errors); - - return obj; - }); - }; }; }; // namespace session::nodeapi diff --git a/include/pro/types.hpp b/include/pro/types.hpp index 7e2e36e..e5d4091 100644 --- a/include/pro/types.hpp +++ b/include/pro/types.hpp @@ -19,6 +19,7 @@ struct toJs_impl { 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()); + obj["signatureHex"] = toJs(env, oxenc::to_hex(pro_proof.sig)); return obj; } diff --git a/include/user_config.hpp b/include/user_config.hpp index 58665b8..c61bdb3 100644 --- a/include/user_config.hpp +++ b/include/user_config.hpp @@ -3,6 +3,7 @@ #include #include "base_config.hpp" +#include "session/config/pro.hpp" #include "session/config/user_profile.hpp" #include "utilities.hpp" @@ -15,6 +16,10 @@ class UserConfigWrapper : public ConfigBaseImpl, public Napi::ObjectWrap pro_config; + int64_t pro_user_features = 0; + config::UserProfile& config{get_config()}; Napi::Value getPriority(const Napi::CallbackInfo& info); @@ -37,8 +42,10 @@ class UserConfigWrapper : public ConfigBaseImpl, public Napi::ObjectWrap toCppBufferView(Napi::Value x, const std::string& std::vector toCppBuffer(Napi::Value x, const std::string& identifier); int64_t toCppInteger(Napi::Value x, const std::string& identifier, bool allowUndefined = false); + +/** + * Same as toCppInteger, but for BigInt + */ +int64_t toCppIntegerB(Napi::Value x, const std::string& identifier, bool allowUndefined = false); + std::optional maybeNonemptyInt(Napi::Value x, const std::string& identifier); std::optional maybeNonemptyBoolean(Napi::Value x, const std::string& identifier); std::optional maybeNonemptySysSeconds( diff --git a/src/encrypt_decrypt/encrypt_decrypt.cpp b/src/encrypt_decrypt/encrypt_decrypt.cpp index dea5220..ee5860f 100644 --- a/src/encrypt_decrypt/encrypt_decrypt.cpp +++ b/src/encrypt_decrypt/encrypt_decrypt.cpp @@ -95,10 +95,10 @@ std::optional> extractProRotatingEd25519PrivKeyAsVect maybeNonemptyString(obj.Get("proRotatingEd25519PrivKey"), identifier); if (proRotatingEd25519PrivKeyHex.has_value() && proRotatingEd25519PrivKeyHex.value().size()) { - assert_length(*proRotatingEd25519PrivKeyHex, 64, identifier); auto ret = from_hex_to_vector(*proRotatingEd25519PrivKeyHex); + assert_length(ret, 64, identifier); return ret; } diff --git a/src/user_config.cpp b/src/user_config.cpp index 8bb2003..bba19ad 100644 --- a/src/user_config.cpp +++ b/src/user_config.cpp @@ -1,5 +1,7 @@ #include "user_config.hpp" +#include + #include #include "base_config.hpp" @@ -10,6 +12,7 @@ #include "session/config/base.hpp" #include "session/config/user_profile.hpp" #include "session/ed25519.hpp" +#include "utilities.hpp" namespace session::nodeapi { @@ -59,6 +62,15 @@ session::config::ProConfig pro_config_from_object(Napi::Object input) { rotating_pubkey_cpp.end(), pro_config.proof.rotating_pubkey.begin()); + // extract backend signature + auto signature_hex_js = proof_js.Get("signatureHex"); + assertIsString(signature_hex_js, "pro_config_from_object.signature_hex_js"); + auto signature_hex_cpp = + toCppString(signature_hex_js, "pro_config_from_object.signature_hex_js"); + auto signature_cpp = from_hex_to_vector(signature_hex_cpp); + std::copy(signature_cpp.begin(), signature_cpp.end(), pro_config.proof.sig.begin()); + assert_length(signature_cpp, 64, "pro_config_from_object.signature_cpp"); + // extract expiryMs assertIsNumber(proof_js.Get("expiryMs"), "pro_config_from_object.expiryMs"); pro_config.proof.expiry_unix_ts = @@ -95,6 +107,10 @@ void UserConfigWrapper::Init(Napi::Env env, Napi::Object exports) { InstanceMethod("setNoteToSelfExpiry", &UserConfigWrapper::setNoteToSelfExpiry), InstanceMethod("getProConfig", &UserConfigWrapper::getProConfig), InstanceMethod("setProConfig", &UserConfigWrapper::setProConfig), + InstanceMethod( + "setProFeaturesBitset", &UserConfigWrapper::setProFeaturesBitset), + InstanceMethod( + "getProFeaturesBitset", &UserConfigWrapper::getProFeaturesBitset), InstanceMethod( "generateProMasterKey", &UserConfigWrapper::generateProMasterKey), InstanceMethod( @@ -251,10 +267,11 @@ void UserConfigWrapper::setNoteToSelfExpiry(const Napi::CallbackInfo& info) { Napi::Value UserConfigWrapper::getProConfig(const Napi::CallbackInfo& info) { return wrapResult(info, [&] { // TODO fixme once extra_data is implemented - // auto pro_config = config.get_pro_config(); - // if (pro_config) { - // return toJs(info.Env(), *pro_config); - // } + + oxen::log::warning(cat, "getProConfig() is not wrapped to libsession"); + if (this->pro_config.has_value()) { + return toJs(info.Env(), this->pro_config); + } return info.Env().Null(); }); @@ -271,6 +288,35 @@ void UserConfigWrapper::setProConfig(const Napi::CallbackInfo& info) { // TODO fixme once extra_data is implemented // config.set_pro_config(pro_config); + this->pro_config = pro_config; + }); +} + +Napi::Value UserConfigWrapper::getProFeaturesBitset(const Napi::CallbackInfo& info) { + return wrapResult(info, [&] { + // TODO fixme once extra_data is implemented + // config.get_pro_features_bitset(); + oxen::log::warning(cat, "getProFeaturesBitset() is not wrapped to libsession"); + return toJs(info.Env(), this->pro_user_features); + }); +} + +void UserConfigWrapper::setProFeaturesBitset(const Napi::CallbackInfo& info) { + wrapExceptions(info, [&] { + assertInfoLength(info, 1); + auto pro_features = info[0]; + assertIsObject(info[0]); + auto obj = info[0].As(); + assertIsBigint(obj.Get("proFeaturesBitset"), "UserConfigWrapper::setProFeaturesBitset"); + + auto pro_user_features_js = obj.Get("proFeaturesBitset"); + auto pro_user_features_cpp = toCppIntegerB( + pro_user_features_js, "UserConfigWrapper::setProFeaturesBitset", false); + + // TODO fixme once extra_data is implemented + + // config.set_pro_features_bitset(pro_user_features_cpp); + this->pro_user_features = pro_user_features_cpp; }); } diff --git a/src/utilities.cpp b/src/utilities.cpp index ddfb230..ae8abaa 100644 --- a/src/utilities.cpp +++ b/src/utilities.cpp @@ -141,6 +141,16 @@ int64_t toCppInteger(Napi::Value x, const std::string& identifier, bool allowUnd throw std::invalid_argument{"Unsupported type for "s + identifier + ": expected a number"}; } +int64_t toCppIntegerB(Napi::Value x, const std::string& identifier, bool allowUndefined) { + auto lossless = true; + if (allowUndefined && (x.IsNull() || x.IsUndefined())) + return 0; + if (x.IsBigInt()) + return x.As().Int64Value(&lossless); + + throw std::invalid_argument{"Unsupported type for "s + identifier + ": expected a bigint"}; +} + std::optional maybeNonemptyInt(Napi::Value x, const std::string& identifier) { if (x.IsNull() || x.IsUndefined()) return std::nullopt; diff --git a/types/pro/pro.d.ts b/types/pro/pro.d.ts index 2823ab0..30790a2 100644 --- a/types/pro/pro.d.ts +++ b/types/pro/pro.d.ts @@ -28,6 +28,11 @@ declare module 'libsession_util_nodejs' { */ rotatingPubkeyHex: string; expiryMs: number; + /** + * signature of the pro proof provided by the backend (hex) + * 64 bytes, 128 chars + */ + signatureHex: string; }; type WithRotatingPrivKeyHex = { @@ -37,6 +42,7 @@ declare module 'libsession_util_nodejs' { rotatingPrivKeyHex: string; }; + type ProConfig = WithRotatingPrivKeyHex & { proProof: ProProof; }; diff --git a/types/user/userconfig.d.ts b/types/user/userconfig.d.ts index a8e0f4c..3cfbd40 100644 --- a/types/user/userconfig.d.ts +++ b/types/user/userconfig.d.ts @@ -41,6 +41,13 @@ declare module 'libsession_util_nodejs' { setProConfig: (proConfig: ProConfig) => void; getProConfig: () => ProConfig | null; + setProFeaturesBitset: (args: WithProFeaturesBitset) => void; + /** + * + * @returns 0 if no pro user features are enabled, the bitset of pro features enabled otherwise + */ + getProFeaturesBitset: () => bigint; + generateProMasterKey: ({ ed25519SeedHex, }: { @@ -84,6 +91,8 @@ declare module 'libsession_util_nodejs' { public setNoteToSelfExpiry: UserConfigWrapper['setNoteToSelfExpiry']; public getProConfig: UserConfigWrapper['getProConfig']; public setProConfig: UserConfigWrapper['setProConfig']; + public getProFeaturesBitset: UserConfigWrapper['getProFeaturesBitset']; + public setProFeaturesBitset: UserConfigWrapper['setProFeaturesBitset']; public generateProMasterKey: UserConfigWrapper['generateProMasterKey']; public generateRotatingPrivKeyHex: UserConfigWrapper['generateRotatingPrivKeyHex']; } @@ -111,6 +120,8 @@ declare module 'libsession_util_nodejs' { | MakeActionCall | MakeActionCall | MakeActionCall + | MakeActionCall + | MakeActionCall | MakeActionCall | MakeActionCall; }